Modularize resolver statistics
am: e655b1d822

Change-Id: Ie03618f5ccb5dce3326844a41ee4b13fc5d1707a
diff --git a/Android.bp b/Android.bp
index bc2c0cd..625e7c8 100644
--- a/Android.bp
+++ b/Android.bp
@@ -55,6 +55,7 @@
         "DnsProxyListener.cpp",
         "DnsResolver.cpp",
         "DnsResolverService.cpp",
+        "DnsStats.cpp",
         "DnsTlsDispatcher.cpp",
         "DnsTlsQueryMap.cpp",
         "DnsTlsTransport.cpp",
@@ -202,6 +203,7 @@
         "resolv_cache_unit_test.cpp",
         "resolv_tls_unit_test.cpp",
         "resolv_unit_test.cpp",
+        "DnsStatsTest.cpp",
     ],
     shared_libs: [
         "libbase",
diff --git a/DnsStats.cpp b/DnsStats.cpp
new file mode 100644
index 0000000..970e30e
--- /dev/null
+++ b/DnsStats.cpp
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2019 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.
+ *
+ */
+
+#define LOG_TAG "resolv"
+
+#include "DnsStats.h"
+
+#include <android-base/logging.h>
+#include <android-base/stringprintf.h>
+
+namespace android::net {
+
+using base::StringPrintf;
+using netdutils::DumpWriter;
+using netdutils::IPAddress;
+using netdutils::IPSockAddr;
+using netdutils::ScopedIndent;
+using std::chrono::duration_cast;
+using std::chrono::microseconds;
+using std::chrono::milliseconds;
+using std::chrono::seconds;
+
+namespace {
+
+static constexpr IPAddress INVALID_IPADDRESS = IPAddress();
+
+std::string rcodeToName(int rcode) {
+    // clang-format off
+    switch (rcode) {
+        case NS_R_NO_ERROR: return "NOERROR";
+        case NS_R_FORMERR: return "FORMERR";
+        case NS_R_SERVFAIL: return "SERVFAIL";
+        case NS_R_NXDOMAIN: return "NXDOMAIN";
+        case NS_R_NOTIMPL: return "NOTIMP";
+        case NS_R_REFUSED: return "REFUSED";
+        case NS_R_YXDOMAIN: return "YXDOMAIN";
+        case NS_R_YXRRSET: return "YXRRSET";
+        case NS_R_NXRRSET: return "NXRRSET";
+        case NS_R_NOTAUTH: return "NOTAUTH";
+        case NS_R_NOTZONE: return "NOTZONE";
+        case NS_R_INTERNAL_ERROR: return "INTERNAL_ERROR";
+        case NS_R_TIMEOUT: return "TIMEOUT";
+        default: return StringPrintf("UNKNOWN(%d)", rcode);
+    }
+    // clang-format on
+}
+
+bool ensureNoInvalidIp(const std::vector<IPSockAddr>& servers) {
+    for (const auto& server : servers) {
+        if (server.ip() == INVALID_IPADDRESS || server.port() == 0) {
+            LOG(WARNING) << "Invalid server: " << server;
+            return false;
+        }
+    }
+    return true;
+}
+
+}  // namespace
+
+// The comparison ignores the last update time.
+bool StatsData::operator==(const StatsData& o) const {
+    return std::tie(serverSockAddr, total, rcodeCounts, latencyUs) ==
+           std::tie(o.serverSockAddr, o.total, o.rcodeCounts, o.latencyUs);
+}
+
+std::string StatsData::toString() const {
+    if (total == 0) return StringPrintf("%s <no data>", serverSockAddr.ip().toString().c_str());
+
+    const auto now = std::chrono::steady_clock::now();
+    const int meanLatencyMs = duration_cast<milliseconds>(latencyUs).count() / total;
+    const int lastUpdateSec = duration_cast<seconds>(now - lastUpdate).count();
+    std::string buf;
+    for (const auto& [rcode, counts] : rcodeCounts) {
+        if (counts != 0) {
+            buf += StringPrintf("%s:%d ", rcodeToName(rcode).c_str(), counts);
+        }
+    }
+    return StringPrintf("%s (%d, %dms, [%s], %ds)", serverSockAddr.ip().toString().c_str(), total,
+                        meanLatencyMs, buf.c_str(), lastUpdateSec);
+}
+
+StatsRecords::StatsRecords(const IPSockAddr& ipSockAddr, size_t size)
+    : mCapacity(size), mStatsData(ipSockAddr) {}
+
+void StatsRecords::push(const Record& record) {
+    updateStatsData(record, true);
+    mRecords.push_back(record);
+
+    if (mRecords.size() > mCapacity) {
+        updateStatsData(mRecords.front(), false);
+        mRecords.pop_front();
+    }
+}
+
+void StatsRecords::updateStatsData(const Record& record, const bool add) {
+    const int rcode = record.rcode;
+    if (add) {
+        mStatsData.total += 1;
+        mStatsData.rcodeCounts[rcode] += 1;
+        mStatsData.latencyUs += record.latencyUs;
+    } else {
+        mStatsData.total -= 1;
+        mStatsData.rcodeCounts[rcode] -= 1;
+        mStatsData.latencyUs -= record.latencyUs;
+    }
+    mStatsData.lastUpdate = std::chrono::steady_clock::now();
+}
+
+bool DnsStats::setServers(const std::vector<netdutils::IPSockAddr>& servers, Protocol protocol) {
+    if (!ensureNoInvalidIp(servers)) return false;
+
+    ServerStatsMap& statsMap = mStats[protocol];
+    for (const auto& server : servers) {
+        statsMap.try_emplace(server, StatsRecords(server, kLogSize));
+    }
+
+    // Clean up the map to eliminate the nodes not belonging to the given list of servers.
+    const auto cleanup = [&](ServerStatsMap* statsMap) {
+        ServerStatsMap tmp;
+        for (const auto& server : servers) {
+            if (statsMap->find(server) != statsMap->end()) {
+                tmp.insert(statsMap->extract(server));
+            }
+        }
+        statsMap->swap(tmp);
+    };
+
+    cleanup(&statsMap);
+
+    return true;
+}
+
+bool DnsStats::addStats(const IPSockAddr& ipSockAddr, const DnsQueryEvent& record) {
+    if (ipSockAddr.ip() == INVALID_IPADDRESS) return false;
+
+    for (auto& [serverSockAddr, statsRecords] : mStats[record.protocol()]) {
+        if (serverSockAddr == ipSockAddr) {
+            const StatsRecords::Record rec = {
+                    .rcode = record.rcode(),
+                    .latencyUs = microseconds(record.latency_micros()),
+            };
+            statsRecords.push(rec);
+            return true;
+        }
+    }
+    return false;
+}
+
+std::vector<StatsData> DnsStats::getStats(Protocol protocol) const {
+    std::vector<StatsData> ret;
+
+    if (mStats.find(protocol) != mStats.end()) {
+        for (const auto& [_, statsRecords] : mStats.at(protocol)) {
+            ret.push_back(statsRecords.getStatsData());
+        }
+    }
+    return ret;
+}
+
+void DnsStats::dump(DumpWriter& dw) {
+    const auto dumpStatsMap = [&](ServerStatsMap& statsMap) {
+        ScopedIndent indentLog(dw);
+        if (statsMap.size() == 0) {
+            dw.println("<no server>");
+            return;
+        }
+        for (const auto& [_, statsRecords] : statsMap) {
+            dw.println("%s", statsRecords.getStatsData().toString().c_str());
+        }
+    };
+
+    dw.println("Server statistics: (total, RTT avg, {rcode:counts}, last update)");
+    ScopedIndent indentStats(dw);
+
+    dw.println("over UDP");
+    dumpStatsMap(mStats[PROTO_UDP]);
+
+    dw.println("over TLS");
+    dumpStatsMap(mStats[PROTO_DOT]);
+
+    dw.println("over TCP");
+    dumpStatsMap(mStats[PROTO_TCP]);
+}
+
+}  // namespace android::net
diff --git a/DnsStats.h b/DnsStats.h
new file mode 100644
index 0000000..526f468
--- /dev/null
+++ b/DnsStats.h
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2019 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.
+ *
+ */
+
+#pragma once
+
+#include <chrono>
+#include <deque>
+#include <map>
+#include <vector>
+
+#include <android-base/thread_annotations.h>
+#include <netdutils/DumpWriter.h>
+#include <netdutils/InternetAddresses.h>
+
+#include "ResolverStats.h"
+#include "stats.pb.h"
+
+namespace android::net {
+
+// The overall information of a StatsRecords.
+struct StatsData {
+    StatsData(const netdutils::IPSockAddr& ipSockAddr) : serverSockAddr(ipSockAddr) {
+        lastUpdate = std::chrono::steady_clock::now();
+    };
+
+    // Server socket address.
+    netdutils::IPSockAddr serverSockAddr;
+
+    // The most recent number of records being accumulated.
+    int total = 0;
+
+    // The map used to store the number of each rcode.
+    std::map<int, int> rcodeCounts;
+
+    // The aggregated RTT in microseconds.
+    // For DNS-over-TCP, it includes TCP handshake.
+    // For DNS-over-TLS, it might include TCP handshake plus SSL handshake.
+    std::chrono::microseconds latencyUs = {};
+
+    // The last update timestamp.
+    std::chrono::time_point<std::chrono::steady_clock> lastUpdate;
+
+    std::string toString() const;
+
+    // For testing.
+    bool operator==(const StatsData& o) const;
+    friend std::ostream& operator<<(std::ostream& os, const StatsData& data) {
+        return os << data.toString();
+    }
+};
+
+// A circular buffer based class used to store the statistics for a server with a protocol.
+class StatsRecords {
+  public:
+    struct Record {
+        int rcode;
+        std::chrono::microseconds latencyUs;
+    };
+
+    StatsRecords(const netdutils::IPSockAddr& ipSockAddr, size_t size);
+
+    void push(const Record& record);
+
+    const StatsData& getStatsData() const { return mStatsData; }
+
+  private:
+    void updateStatsData(const Record& record, const bool add);
+
+    std::deque<Record> mRecords;
+    size_t mCapacity;
+    StatsData mStatsData;
+};
+
+// DnsStats class manages the statistics of DNS servers per netId.
+// The class itself is not thread-safe.
+class DnsStats {
+  public:
+    using ServerStatsMap = std::map<netdutils::IPSockAddr, StatsRecords>;
+
+    // Add |servers| to the map, and remove no-longer-used servers.
+    // Return true if they are successfully added; otherwise, return false.
+    bool setServers(const std::vector<netdutils::IPSockAddr>& servers, Protocol protocol);
+
+    // Return true if |record| is successfully added into |server|'s stats; otherwise, return false.
+    bool addStats(const netdutils::IPSockAddr& server, const DnsQueryEvent& record);
+
+    void dump(netdutils::DumpWriter& dw);
+
+    // For testing.
+    std::vector<StatsData> getStats(Protocol protocol) const;
+
+    // TODO: Compatible support for getResolverInfo().
+    // TODO: Support getSortedServers().
+
+  private:
+    std::map<Protocol, ServerStatsMap> mStats;
+
+    static constexpr size_t kLogSize = 128;
+};
+
+}  // namespace android::net
diff --git a/DnsStatsTest.cpp b/DnsStatsTest.cpp
new file mode 100644
index 0000000..525b611
--- /dev/null
+++ b/DnsStatsTest.cpp
@@ -0,0 +1,292 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include "DnsStats.h"
+
+namespace android::net {
+
+using namespace std::chrono_literals;
+using android::netdutils::IPSockAddr;
+using std::chrono::milliseconds;
+using ::testing::IsEmpty;
+using ::testing::UnorderedElementsAreArray;
+
+namespace {
+
+DnsQueryEvent makeDnsQueryEvent(const Protocol protocol, const NsRcode rcode,
+                                const milliseconds& latency) {
+    DnsQueryEvent event;
+    event.set_protocol(protocol);
+    event.set_rcode(rcode);
+    event.set_latency_micros(latency.count() * 1000);
+    return event;
+}
+
+StatsData makeStatsData(const IPSockAddr& server, const int total, const milliseconds& latencyMs,
+                        const std::map<int, int>& rcodeCounts) {
+    StatsData ret(server);
+    ret.total = total;
+    ret.latencyUs = latencyMs;
+    ret.rcodeCounts = rcodeCounts;
+    return ret;
+}
+
+}  // namespace
+
+class StatsRecordsTest : public ::testing::Test {};
+
+TEST_F(StatsRecordsTest, PushRecord) {
+    const IPSockAddr server = IPSockAddr::toIPSockAddr("127.0.0.2", 53);
+    constexpr size_t size = 3;
+    const StatsRecords::Record recordNoError = {NS_R_NO_ERROR, 10ms};
+    const StatsRecords::Record recordTimeout = {NS_R_TIMEOUT, 250ms};
+
+    StatsRecords sr(server, size);
+    EXPECT_EQ(sr.getStatsData(), makeStatsData(server, 0, 0ms, {}));
+
+    sr.push(recordNoError);
+    EXPECT_EQ(sr.getStatsData(), makeStatsData(server, 1, 10ms, {{NS_R_NO_ERROR, 1}}));
+
+    sr.push(recordNoError);
+    EXPECT_EQ(sr.getStatsData(), makeStatsData(server, 2, 20ms, {{NS_R_NO_ERROR, 2}}));
+
+    sr.push(recordTimeout);
+    EXPECT_EQ(sr.getStatsData(),
+              makeStatsData(server, 3, 270ms, {{NS_R_NO_ERROR, 2}, {NS_R_TIMEOUT, 1}}));
+
+    sr.push(recordTimeout);
+    EXPECT_EQ(sr.getStatsData(),
+              makeStatsData(server, 3, 510ms, {{NS_R_NO_ERROR, 1}, {NS_R_TIMEOUT, 2}}));
+
+    sr.push(recordTimeout);
+    EXPECT_EQ(sr.getStatsData(),
+              makeStatsData(server, 3, 750ms, {{NS_R_NO_ERROR, 0}, {NS_R_TIMEOUT, 3}}));
+}
+
+class DnsStatsTest : public ::testing::Test {
+  protected:
+    DnsStats mDnsStats;
+};
+
+TEST_F(DnsStatsTest, SetServers) {
+    static const struct {
+        std::vector<std::string> servers;
+        std::vector<std::string> expectation;
+        bool isSuccess;
+    } tests[] = {
+            // Normal case.
+            {
+                    {"127.0.0.1", "127.0.0.2", "fe80::1%22", "2001:db8::2", "::1"},
+                    {"127.0.0.1", "127.0.0.2", "fe80::1%22", "2001:db8::2", "::1"},
+                    true,
+            },
+            // Duplicate servers.
+            {
+                    {"127.0.0.1", "2001:db8::2", "127.0.0.1", "2001:db8::2"},
+                    {"127.0.0.1", "2001:db8::2"},
+                    true,
+            },
+            // Invalid server addresses. The state remains in previous state.
+            {
+                    {"not_an_ip", "127.0.0.3", "127.a.b.2"},
+                    {"127.0.0.1", "2001:db8::2"},
+                    false,
+            },
+            // Clean up the old servers 127.0.0.1 and 127.0.0.2.
+            {
+                    {"127.0.0.4", "2001:db8::5"},
+                    {"127.0.0.4", "2001:db8::5"},
+                    true,
+            },
+            // Empty list.
+            {{}, {}, true},
+    };
+
+    for (const auto& [servers, expectation, isSuccess] : tests) {
+        std::vector<IPSockAddr> ipSockAddrs;
+        ipSockAddrs.reserve(servers.size());
+        for (const auto& server : servers) {
+            ipSockAddrs.push_back(IPSockAddr::toIPSockAddr(server, 53));
+        }
+
+        EXPECT_TRUE(mDnsStats.setServers(ipSockAddrs, PROTO_TCP) == isSuccess);
+        EXPECT_TRUE(mDnsStats.setServers(ipSockAddrs, PROTO_UDP) == isSuccess);
+        EXPECT_TRUE(mDnsStats.setServers(ipSockAddrs, PROTO_DOT) == isSuccess);
+
+        std::vector<StatsData> expectedStats;
+        expectedStats.reserve(expectation.size());
+        for (const auto& exp : expectation) {
+            expectedStats.push_back(makeStatsData(IPSockAddr::toIPSockAddr(exp, 53), 0, 0ms, {}));
+        }
+
+        EXPECT_THAT(mDnsStats.getStats(PROTO_TCP), UnorderedElementsAreArray(expectedStats));
+        EXPECT_THAT(mDnsStats.getStats(PROTO_UDP), UnorderedElementsAreArray(expectedStats));
+        EXPECT_THAT(mDnsStats.getStats(PROTO_DOT), UnorderedElementsAreArray(expectedStats));
+    }
+}
+
+TEST_F(DnsStatsTest, SetServersDifferentPorts) {
+    const std::vector<IPSockAddr> servers = {
+            IPSockAddr::toIPSockAddr("127.0.0.1", 0),   IPSockAddr::toIPSockAddr("fe80::1", 0),
+            IPSockAddr::toIPSockAddr("127.0.0.1", 53),  IPSockAddr::toIPSockAddr("127.0.0.1", 5353),
+            IPSockAddr::toIPSockAddr("127.0.0.1", 853), IPSockAddr::toIPSockAddr("fe80::1", 53),
+            IPSockAddr::toIPSockAddr("fe80::1", 5353),  IPSockAddr::toIPSockAddr("fe80::1", 853),
+    };
+
+    // Servers setup fails due to port unset.
+    EXPECT_FALSE(mDnsStats.setServers(servers, PROTO_TCP));
+    EXPECT_FALSE(mDnsStats.setServers(servers, PROTO_UDP));
+    EXPECT_FALSE(mDnsStats.setServers(servers, PROTO_DOT));
+
+    EXPECT_THAT(mDnsStats.getStats(PROTO_TCP), IsEmpty());
+    EXPECT_THAT(mDnsStats.getStats(PROTO_UDP), IsEmpty());
+    EXPECT_THAT(mDnsStats.getStats(PROTO_DOT), IsEmpty());
+
+    EXPECT_TRUE(mDnsStats.setServers(std::vector(servers.begin() + 2, servers.end()), PROTO_TCP));
+    EXPECT_TRUE(mDnsStats.setServers(std::vector(servers.begin() + 2, servers.end()), PROTO_UDP));
+    EXPECT_TRUE(mDnsStats.setServers(std::vector(servers.begin() + 2, servers.end()), PROTO_DOT));
+
+    const std::vector<StatsData> expectedStats = {
+            makeStatsData(servers[2], 0, 0ms, {}), makeStatsData(servers[3], 0, 0ms, {}),
+            makeStatsData(servers[4], 0, 0ms, {}), makeStatsData(servers[5], 0, 0ms, {}),
+            makeStatsData(servers[6], 0, 0ms, {}), makeStatsData(servers[7], 0, 0ms, {}),
+    };
+
+    EXPECT_THAT(mDnsStats.getStats(PROTO_TCP), UnorderedElementsAreArray(expectedStats));
+    EXPECT_THAT(mDnsStats.getStats(PROTO_UDP), UnorderedElementsAreArray(expectedStats));
+    EXPECT_THAT(mDnsStats.getStats(PROTO_DOT), UnorderedElementsAreArray(expectedStats));
+}
+
+TEST_F(DnsStatsTest, AddStatsAndClear) {
+    const std::vector<IPSockAddr> servers = {
+            IPSockAddr::toIPSockAddr("127.0.0.1", 53),
+            IPSockAddr::toIPSockAddr("127.0.0.2", 53),
+    };
+    const DnsQueryEvent record = makeDnsQueryEvent(PROTO_UDP, NS_R_NO_ERROR, 10ms);
+
+    EXPECT_TRUE(mDnsStats.setServers(servers, PROTO_TCP));
+    EXPECT_TRUE(mDnsStats.setServers(servers, PROTO_UDP));
+
+    // Fail to add stats because of incorrect arguments.
+    EXPECT_FALSE(mDnsStats.addStats(IPSockAddr::toIPSockAddr("127.0.0.4", 53), record));
+    EXPECT_FALSE(mDnsStats.addStats(IPSockAddr::toIPSockAddr("127.a.b.4", 53), record));
+
+    EXPECT_TRUE(mDnsStats.addStats(servers[0], record));
+    EXPECT_TRUE(mDnsStats.addStats(servers[0], record));
+    EXPECT_TRUE(mDnsStats.addStats(servers[1], record));
+
+    const std::vector<StatsData> expectedStatsForTcp = {
+            makeStatsData(servers[0], 0, 0ms, {}),
+            makeStatsData(servers[1], 0, 0ms, {}),
+    };
+    const std::vector<StatsData> expectedStatsForUdp = {
+            makeStatsData(servers[0], 2, 20ms, {{NS_R_NO_ERROR, 2}}),
+            makeStatsData(servers[1], 1, 10ms, {{NS_R_NO_ERROR, 1}}),
+    };
+
+    EXPECT_THAT(mDnsStats.getStats(PROTO_TCP), UnorderedElementsAreArray(expectedStatsForTcp));
+    EXPECT_THAT(mDnsStats.getStats(PROTO_UDP), UnorderedElementsAreArray(expectedStatsForUdp));
+    EXPECT_THAT(mDnsStats.getStats(PROTO_DOT), IsEmpty());
+
+    // Clear stats.
+    EXPECT_TRUE(mDnsStats.setServers({}, PROTO_TCP));
+    EXPECT_TRUE(mDnsStats.setServers({}, PROTO_UDP));
+    EXPECT_TRUE(mDnsStats.setServers({}, PROTO_DOT));
+    EXPECT_THAT(mDnsStats.getStats(PROTO_TCP), IsEmpty());
+    EXPECT_THAT(mDnsStats.getStats(PROTO_UDP), IsEmpty());
+    EXPECT_THAT(mDnsStats.getStats(PROTO_DOT), IsEmpty());
+}
+
+TEST_F(DnsStatsTest, StatsRemainsInExistentServer) {
+    std::vector<IPSockAddr> servers = {
+            IPSockAddr::toIPSockAddr("127.0.0.1", 53),
+            IPSockAddr::toIPSockAddr("127.0.0.2", 53),
+    };
+    const DnsQueryEvent recordNoError = makeDnsQueryEvent(PROTO_UDP, NS_R_NO_ERROR, 10ms);
+    const DnsQueryEvent recordTimeout = makeDnsQueryEvent(PROTO_UDP, NS_R_TIMEOUT, 250ms);
+
+    EXPECT_TRUE(mDnsStats.setServers(servers, PROTO_UDP));
+
+    // Add a record to 127.0.0.1.
+    EXPECT_TRUE(mDnsStats.addStats(servers[0], recordNoError));
+
+    // Add four records to 127.0.0.2.
+    EXPECT_TRUE(mDnsStats.addStats(servers[1], recordNoError));
+    EXPECT_TRUE(mDnsStats.addStats(servers[1], recordNoError));
+    EXPECT_TRUE(mDnsStats.addStats(servers[1], recordTimeout));
+    EXPECT_TRUE(mDnsStats.addStats(servers[1], recordTimeout));
+
+    std::vector<StatsData> expectedStats = {
+            makeStatsData(servers[0], 1, 10ms, {{NS_R_NO_ERROR, 1}}),
+            makeStatsData(servers[1], 4, 520ms, {{NS_R_NO_ERROR, 2}, {NS_R_TIMEOUT, 2}}),
+    };
+    EXPECT_THAT(mDnsStats.getStats(PROTO_UDP), UnorderedElementsAreArray(expectedStats));
+
+    // Update the server list, the stats of 127.0.0.2 will remain.
+    servers = {
+            IPSockAddr::toIPSockAddr("127.0.0.2", 53),
+            IPSockAddr::toIPSockAddr("127.0.0.3", 53),
+            IPSockAddr::toIPSockAddr("127.0.0.4", 53),
+    };
+    EXPECT_TRUE(mDnsStats.setServers(servers, PROTO_UDP));
+    expectedStats = {
+            makeStatsData(servers[0], 4, 520ms, {{NS_R_NO_ERROR, 2}, {NS_R_TIMEOUT, 2}}),
+            makeStatsData(servers[1], 0, 0ms, {}),
+            makeStatsData(servers[2], 0, 0ms, {}),
+    };
+    EXPECT_THAT(mDnsStats.getStats(PROTO_UDP), UnorderedElementsAreArray(expectedStats));
+
+    // Let's add a record to 127.0.0.2 again.
+    EXPECT_TRUE(mDnsStats.addStats(servers[0], recordNoError));
+    expectedStats = {
+            makeStatsData(servers[0], 5, 530ms, {{NS_R_NO_ERROR, 3}, {NS_R_TIMEOUT, 2}}),
+            makeStatsData(servers[1], 0, 0ms, {}),
+            makeStatsData(servers[2], 0, 0ms, {}),
+    };
+    EXPECT_THAT(mDnsStats.getStats(PROTO_UDP), UnorderedElementsAreArray(expectedStats));
+}
+
+TEST_F(DnsStatsTest, AddStatsRecords_100000) {
+    constexpr int num = 100000;
+    const std::vector<IPSockAddr> servers = {
+            IPSockAddr::toIPSockAddr("127.0.0.1", 53),
+            IPSockAddr::toIPSockAddr("127.0.0.2", 53),
+            IPSockAddr::toIPSockAddr("127.0.0.3", 53),
+            IPSockAddr::toIPSockAddr("127.0.0.4", 53),
+    };
+
+    EXPECT_TRUE(mDnsStats.setServers(servers, PROTO_TCP));
+    EXPECT_TRUE(mDnsStats.setServers(servers, PROTO_UDP));
+    EXPECT_TRUE(mDnsStats.setServers(servers, PROTO_DOT));
+
+    for (int i = 0; i < num; i++) {
+        const auto eventTcp = makeDnsQueryEvent(PROTO_TCP, static_cast<NsRcode>(i % 10), 10ms);
+        const auto eventUdp = makeDnsQueryEvent(PROTO_UDP, static_cast<NsRcode>(i % 10), 10ms);
+        const auto eventDot = makeDnsQueryEvent(PROTO_DOT, static_cast<NsRcode>(i % 10), 10ms);
+        for (const auto& server : servers) {
+            const std::string trace = server.toString() + "-" + std::to_string(i);
+            SCOPED_TRACE(trace);
+            EXPECT_TRUE(mDnsStats.addStats(server, eventTcp));
+            EXPECT_TRUE(mDnsStats.addStats(server, eventUdp));
+            EXPECT_TRUE(mDnsStats.addStats(server, eventDot));
+        }
+    }
+}
+
+}  // namespace android::net
diff --git a/DnsTlsDispatcher.cpp b/DnsTlsDispatcher.cpp
index 58ef2a7..7ca0464 100644
--- a/DnsTlsDispatcher.cpp
+++ b/DnsTlsDispatcher.cpp
@@ -17,8 +17,11 @@
 #define LOG_TAG "resolv"
 
 #include "DnsTlsDispatcher.h"
+
 #include <netdutils/Stopwatch.h>
+
 #include "DnsTlsSocketFactory.h"
+#include "resolv_cache.h"
 #include "resolv_private.h"
 #include "stats.pb.h"
 
@@ -27,6 +30,7 @@
 namespace android {
 namespace net {
 
+using android::netdutils::IPSockAddr;
 using android::netdutils::Stopwatch;
 using netdutils::Slice;
 
@@ -112,19 +116,23 @@
             case DnsTlsTransport::Response::success:
                 dnsQueryEvent->set_rcode(
                         static_cast<NsRcode>(reinterpret_cast<HEADER*>(ans.base())->rcode));
+                resolv_stats_add(statp->netid, IPSockAddr::toIPSockAddr(server.ss), dnsQueryEvent);
                 return code;
             case DnsTlsTransport::Response::limit_error:
                 dnsQueryEvent->set_rcode(NS_R_INTERNAL_ERROR);
+                resolv_stats_add(statp->netid, IPSockAddr::toIPSockAddr(server.ss), dnsQueryEvent);
                 return code;
             // These response codes might differ when trying other servers, so
             // keep iterating to see if we can get a different (better) result.
             case DnsTlsTransport::Response::network_error:
                 // Sync from res_tls_send in res_send.cpp
                 dnsQueryEvent->set_rcode(NS_R_TIMEOUT);
-                continue;
+                resolv_stats_add(statp->netid, IPSockAddr::toIPSockAddr(server.ss), dnsQueryEvent);
+                break;
             case DnsTlsTransport::Response::internal_error:
                 dnsQueryEvent->set_rcode(NS_R_INTERNAL_ERROR);
-                continue;
+                resolv_stats_add(statp->netid, IPSockAddr::toIPSockAddr(server.ss), dnsQueryEvent);
+                break;
             // No "default" statement.
         }
     }
diff --git a/PrivateDnsConfiguration.cpp b/PrivateDnsConfiguration.cpp
index 9242222..f59e64e 100644
--- a/PrivateDnsConfiguration.cpp
+++ b/PrivateDnsConfiguration.cpp
@@ -28,6 +28,7 @@
 #include "ResolverEventReporter.h"
 #include "netd_resolv/resolv.h"
 #include "netdutils/BackoffSequence.h"
+#include "resolv_cache.h"
 
 using std::chrono::milliseconds;
 
@@ -95,6 +96,7 @@
     } else {
         mPrivateDnsModes[netId] = PrivateDnsMode::OFF;
         mPrivateDnsTransports.erase(netId);
+        resolv_stats_set_servers_for_dot(netId, {});
         return 0;
     }
 
@@ -126,7 +128,8 @@
             validatePrivateDnsProvider(server, tracker, netId, mark);
         }
     }
-    return 0;
+
+    return resolv_stats_set_servers_for_dot(netId, servers);
 }
 
 PrivateDnsStatus PrivateDnsConfiguration::getStatus(unsigned netId) {
diff --git a/ResolverController.cpp b/ResolverController.cpp
index 6927e5f..5fecc61 100644
--- a/ResolverController.cpp
+++ b/ResolverController.cpp
@@ -356,6 +356,7 @@
             dw.decIndent();
         }
         dw.println("Concurrent DNS query timeout: %d", wait_for_pending_req_timeout_count[0]);
+        resolv_stats_dump(dw, netId);
     }
     dw.decIndent();
 }
diff --git a/res_cache.cpp b/res_cache.cpp
index d26ad76..56b15e7 100644
--- a/res_cache.cpp
+++ b/res_cache.cpp
@@ -58,10 +58,15 @@
 
 #include <server_configurable_flags/get_flags.h>
 
+#include "DnsStats.h"
 #include "res_debug.h"
 #include "resolv_private.h"
 
 using android::base::StringAppendF;
+using android::net::DnsQueryEvent;
+using android::net::DnsStats;
+using android::netdutils::DumpWriter;
+using android::netdutils::IPSockAddr;
 
 /* This code implements a small and *simple* DNS resolver cache.
  *
@@ -939,6 +944,7 @@
     int wait_for_pending_req_timeout_count;
     // Map format: ReturnCode:rate_denom
     std::unordered_map<int, uint32_t> dns_event_subsampling_map;
+    std::unique_ptr<DnsStats> dnsStats;
 };
 
 /* gets cache associated with a network, or NULL if none exists */
@@ -1362,6 +1368,7 @@
     cache_info->netid = netid;
     cache_info->cache = new Cache;
     cache_info->dns_event_subsampling_map = resolv_get_dns_event_subsampling_map();
+    cache_info->dnsStats.reset(new DnsStats());
     insert_cache_info_locked(cache_info);
 
     return 0;
@@ -1379,6 +1386,11 @@
             prev_cache_info->next = cache_info->next;
             delete cache_info->cache;
             free_nameservers_locked(cache_info);
+
+            // It won't be necessary after the memory of cache_info can be deallocated by the
+            // C++ delete expression.
+            cache_info->dnsStats.reset();
+
             free(cache_info);
             break;
         }
@@ -1539,6 +1551,18 @@
     // since the stored cache entries do contain the domain, not just the host name.
     cache_info->search_domains = filter_domains(domains);
 
+    std::vector<IPSockAddr> serverSockAddrs;
+    serverSockAddrs.reserve(cache_info->nameservers.size());
+    for (const auto& server : cache_info->nameservers) {
+        serverSockAddrs.push_back(IPSockAddr::toIPSockAddr(server, 53));
+    }
+
+    if (!cache_info->dnsStats->setServers(serverSockAddrs, android::net::PROTO_TCP) ||
+        !cache_info->dnsStats->setServers(serverSockAddrs, android::net::PROTO_UDP)) {
+        LOG(WARNING) << __func__ << ": netid = " << netid << ", failed to set dns stats";
+        return -EINVAL;
+    }
+
     return 0;
 }
 
@@ -1780,3 +1804,41 @@
     *expiration = e->expires;
     return 0;
 }
+
+int resolv_stats_set_servers_for_dot(unsigned netid, const std::vector<std::string>& servers) {
+    std::lock_guard guard(cache_mutex);
+    const auto info = find_cache_info_locked(netid);
+
+    if (info == nullptr) return -ENONET;
+
+    std::vector<IPSockAddr> serverSockAddrs;
+    serverSockAddrs.reserve(servers.size());
+    for (const auto& server : servers) {
+        serverSockAddrs.push_back(IPSockAddr::toIPSockAddr(server, 853));
+    }
+
+    if (!info->dnsStats->setServers(serverSockAddrs, android::net::PROTO_DOT)) {
+        LOG(WARNING) << __func__ << ": netid = " << netid << ", failed to set dns stats";
+        return -EINVAL;
+    }
+
+    return 0;
+}
+
+bool resolv_stats_add(unsigned netid, const android::netdutils::IPSockAddr& server,
+                      const DnsQueryEvent* record) {
+    if (record == nullptr) return false;
+
+    std::lock_guard guard(cache_mutex);
+    if (const auto info = find_cache_info_locked(netid); info != nullptr) {
+        return info->dnsStats->addStats(server, *record);
+    }
+    return false;
+}
+
+void resolv_stats_dump(DumpWriter& dw, unsigned netid) {
+    std::lock_guard guard(cache_mutex);
+    if (const auto info = find_cache_info_locked(netid); info != nullptr) {
+        info->dnsStats->dump(dw);
+    }
+}
diff --git a/res_send.cpp b/res_send.cpp
index 9eee05d..1dc5f57 100644
--- a/res_send.cpp
+++ b/res_send.cpp
@@ -130,6 +130,7 @@
 using android::net::PrivateDnsStatus;
 using android::net::PROTO_TCP;
 using android::net::PROTO_UDP;
+using android::netdutils::IPSockAddr;
 using android::netdutils::Slice;
 using android::netdutils::Stopwatch;
 
@@ -544,6 +545,7 @@
                     _res_stats_set_sample(&sample, now, *rcode, delay);
                     _resolv_cache_add_resolver_stats_sample(statp->netid, revision_id, ns, &sample,
                                                             params.max_samples);
+                    resolv_stats_add(statp->netid, IPSockAddr::toIPSockAddr(*nsap), dnsQueryEvent);
                 }
 
                 LOG(INFO) << __func__ << ": used send_vc " << n;
@@ -577,6 +579,7 @@
                     _res_stats_set_sample(&sample, now, *rcode, delay);
                     _resolv_cache_add_resolver_stats_sample(statp->netid, revision_id, ns, &sample,
                                                             params.max_samples);
+                    resolv_stats_add(statp->netid, IPSockAddr::toIPSockAddr(*nsap), dnsQueryEvent);
                 }
 
                 LOG(INFO) << __func__ << ": used send_dg " << n;
diff --git a/resolv_cache.h b/resolv_cache.h
index 2b372f6..6ede2e0 100644
--- a/resolv_cache.h
+++ b/resolv_cache.h
@@ -28,13 +28,16 @@
 
 #pragma once
 
-#include "netd_resolv/resolv.h"
-
-#include <stddef.h>
-
 #include <unordered_map>
 #include <vector>
 
+#include <netdutils/DumpWriter.h>
+#include <netdutils/InternetAddresses.h>
+#include <stats.pb.h>
+
+#include "ResolverStats.h"
+#include "netd_resolv/params.h"
+
 // Sets the name server addresses to the provided ResState.
 // The name servers are retrieved from the cache which is associated
 // with the network to which ResState is associated.
@@ -83,3 +86,12 @@
 // Get the expiration time of a cache entry. Return 0 on success; otherwise, an negative error is
 // returned if the expiration time can't be acquired.
 int resolv_cache_get_expiration(unsigned netid, const std::vector<char>& query, time_t* expiration);
+
+// Set private DNS servers to DnsStats for a given network.
+int resolv_stats_set_servers_for_dot(unsigned netid, const std::vector<std::string>& servers);
+
+// Add a statistics record to DnsStats for a given network.
+bool resolv_stats_add(unsigned netid, const android::netdutils::IPSockAddr& server,
+                      const android::net::DnsQueryEvent* record);
+
+void resolv_stats_dump(android::netdutils::DumpWriter& dw, unsigned netid);
diff --git a/resolv_private.h b/resolv_private.h
index 3272844..ea7cdc2 100644
--- a/resolv_private.h
+++ b/resolv_private.h
@@ -104,7 +104,6 @@
 };
 
 // TODO: remove these legacy aliases
-typedef ResState __res_state;
 typedef ResState* res_state;
 
 /* Retrieve a local copy of the stats for the given netid. The buffer must have space for