Modularize resolver statistics

The extendibility of the legacy resolver statistics is limited because
it was originally designed for libc, which is hard to implement to
support other types of DNS, e.g. DNS-over-TLS. Therefore, DnsStats
is introduced to manage the statistics in a more general way.

DoT statistics is now available from DnsStats and is shown in
dumpsys dnsresolver.

Example of output from dumpsys dnsresolver:

Server statistics: (total, RTT avg, {rcode:counts}, last update)
  over UDP
    8.8.4.4 <no data>
    8.8.8.8 <no data>
    2001:4860:4860::8844 (7, 2201ms, [NOERROR:4 TIMEOUT:3 ], 2s)
    2001:4860:4860::8888 (3, 11ms, [NOERROR:3 ], 4s)
  over TLS
    <no server>
  over TCP
    8.8.4.4 <no data>
    8.8.8.8 <no data>
    2001:4860:4860::8844 <no data>
    2001:4860:4860::8888 <no data>

Bug: 140286585
Test: atest --include-subdirs packages/modules/DnsResolver
Test: checked output in dumpsys dnsresolver
Change-Id: I73f0108d5e9bb493cf7eda68252f5a0922149a98
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