Skip to content

Commit 1177916

Browse files
committed
Pull request 389: AGDNS-2627-query-statistics
Merge in GO/dnsproxy from AGDNS-2627-query-statistics to master Squashed commit of the following: commit 32a4b13 Author: Stanislav Chzhen <[email protected]> Date: Wed Jan 29 20:10:48 2025 +0300 proxy: imp docs commit 4769b73 Merge: 79703fd 9621ccd Author: Stanislav Chzhen <[email protected]> Date: Wed Jan 29 20:03:23 2025 +0300 Merge branch 'master' into AGDNS-2627-query-statistics commit 79703fd Author: Stanislav Chzhen <[email protected]> Date: Wed Jan 29 19:31:03 2025 +0300 proxy: imp docs commit 13a8241 Author: Stanislav Chzhen <[email protected]> Date: Thu Jan 23 15:46:19 2025 +0300 proxy: imp docs commit af2e4a9 Author: Stanislav Chzhen <[email protected]> Date: Wed Jan 22 17:44:34 2025 +0300 proxy: imp naming commit b877057 Author: Stanislav Chzhen <[email protected]> Date: Wed Jan 22 16:44:00 2025 +0300 proxy: imp code commit ef492d4 Author: Stanislav Chzhen <[email protected]> Date: Tue Jan 21 21:08:12 2025 +0300 proxy: fix test commit 0aa04b3 Author: Stanislav Chzhen <[email protected]> Date: Tue Jan 21 20:16:37 2025 +0300 proxy: imp docs commit 966cabf Author: Stanislav Chzhen <[email protected]> Date: Thu Jan 16 20:48:05 2025 +0300 all: add tests commit 4f686ad Author: Stanislav Chzhen <[email protected]> Date: Thu Jan 16 15:54:01 2025 +0300 proxy: query statistics
1 parent 9621ccd commit 1177916

File tree

7 files changed

+519
-23
lines changed

7 files changed

+519
-23
lines changed

proxy/dnscontext.go

+34-9
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"net"
55
"net/http"
66
"net/netip"
7-
"time"
87

98
"github.com/AdguardTeam/dnsproxy/upstream"
109
"github.com/ameshkov/dnscrypt/v2"
@@ -47,17 +46,19 @@ type DNSContext struct {
4746
// servers if it's not nil.
4847
CustomUpstreamConfig *CustomUpstreamConfig
4948

49+
// queryStatistics contains the DNS query statistics for both the upstream
50+
// and fallback DNS servers.
51+
queryStatistics *QueryStatistics
52+
5053
// Req is the request message.
5154
Req *dns.Msg
55+
5256
// Res is the response message.
5357
Res *dns.Msg
5458

59+
// Proto is the DNS protocol of the query.
5560
Proto Proto
5661

57-
// CachedUpstreamAddr is the address of the upstream which the answer was
58-
// cached with. It's empty for responses resolved by the upstream server.
59-
CachedUpstreamAddr string
60-
6162
// RequestedPrivateRDNS is the subnet extracted from the ARPA domain of
6263
// request's question if it's a PTR, SOA, or NS query for a private IP
6364
// address. It can be a single-address subnet as well as a zero-length one.
@@ -69,10 +70,6 @@ type DNSContext struct {
6970
// Addr is the address of the client.
7071
Addr netip.AddrPort
7172

72-
// QueryDuration is the duration of a successful query to an upstream
73-
// server or, if the upstream server is unavailable, to a fallback server.
74-
QueryDuration time.Duration
75-
7673
// DoQVersion is the DoQ protocol version. It can (and should) be read from
7774
// ALPN, but in the current version we also use the way DNS messages are
7875
// encoded as a signal.
@@ -115,6 +112,34 @@ func (p *Proxy) newDNSContext(proto Proto, req *dns.Msg, addr netip.AddrPort) (d
115112
}
116113
}
117114

115+
// QueryStatistics returns the DNS query statistics for both the upstream and
116+
// fallback DNS servers. The returned statistics will be nil until a DNS lookup
117+
// has been performed.
118+
//
119+
// Depending on whether the DNS request was successfully resolved and the
120+
// upstream mode, the returned statistics consist of:
121+
//
122+
// - If the query was successfully resolved, the statistics contain the DNS
123+
// lookup duration for the main resolver.
124+
//
125+
// - If the query was retrieved from the cache, the statistics will contain a
126+
// single entry of [UpstreamStatistics] where the property IsCached is set
127+
// to true.
128+
//
129+
// - If the upstream mode is [UpstreamModeFastestAddr] and the query was
130+
// successfully resolved, the statistics contain the DNS lookup durations or
131+
// errors for each main upstream.
132+
//
133+
// - If the query was resolved by the fallback resolver, the statistics
134+
// contain the DNS lookup errors for each main upstream and the query
135+
// duration for the fallback resolver.
136+
//
137+
// - If the query was not resolved at all, the statistics contain the DNS
138+
// lookup errors for each main and fallback resolvers.
139+
func (dctx *DNSContext) QueryStatistics() (s *QueryStatistics) {
140+
return dctx.queryStatistics
141+
}
142+
118143
// calcFlagsAndSize lazily calculates some values required for Resolve method.
119144
func (dctx *DNSContext) calcFlagsAndSize() {
120145
if dctx.udpSize != 0 || dctx.Req == nil {

proxy/exchange.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@ func (p *Proxy) exchangeUpstreams(
3535
if len(ups) == 1 {
3636
u = ups[0]
3737
resp, _, err = p.exchange(u, req, p.time)
38-
// TODO(e.burkov): p.updateRTT(u.Address(), elapsed)
38+
if err != nil {
39+
return nil, nil, err
40+
}
41+
42+
// TODO(e.burkov): Consider updating the RTT of a single upstream.
3943

4044
return resp, u, err
4145
}

proxy/proxy.go

+11-9
Original file line numberDiff line numberDiff line change
@@ -554,42 +554,44 @@ func (p *Proxy) replyFromUpstream(d *DNSContext) (ok bool, err error) {
554554
p.recDetector.add(d.Req)
555555
}
556556

557-
start := time.Now()
558557
src := "upstream"
558+
wrapped := upstreamsWithStats(upstreams)
559559

560560
// Perform the DNS request.
561-
resp, u, err := p.exchangeUpstreams(req, upstreams)
562-
if dns64Ups := p.performDNS64(req, resp, upstreams); dns64Ups != nil {
561+
resp, u, err := p.exchangeUpstreams(req, wrapped)
562+
if dns64Ups := p.performDNS64(req, resp, wrapped); dns64Ups != nil {
563563
u = dns64Ups
564564
} else if p.isBogusNXDomain(resp) {
565565
p.logger.Debug("response contains bogus-nxdomain ip")
566566
resp = p.messages.NewMsgNXDOMAIN(req)
567567
}
568568

569+
var wrappedFallbacks []upstream.Upstream
569570
if err != nil && !isPrivate && p.Fallbacks != nil {
570571
p.logger.Debug("using fallback", slogutil.KeyError, err)
571572

572-
// Reset the timer.
573-
start = time.Now()
574573
src = "fallback"
575574

576575
// upstreams mustn't appear empty since they have been validated when
577576
// creating proxy.
578577
upstreams = p.Fallbacks.getUpstreamsForDomain(req.Question[0].Name)
579578

580-
resp, u, err = upstream.ExchangeParallel(upstreams, req)
579+
wrappedFallbacks = upstreamsWithStats(upstreams)
580+
resp, u, err = upstream.ExchangeParallel(wrappedFallbacks, req)
581581
}
582582

583583
if err != nil {
584584
p.logger.Debug("resolving err", "src", src, slogutil.KeyError, err)
585585
}
586586

587587
if resp != nil {
588-
d.QueryDuration = time.Since(start)
589-
p.logger.Debug("resolved", "src", src, "rtt", d.QueryDuration)
588+
p.logger.Debug("resolved", "src", src)
590589
}
591590

592-
p.handleExchangeResult(d, req, resp, u)
591+
unwrapped, stats := collectQueryStats(p.UpstreamMode, u, wrapped, wrappedFallbacks)
592+
d.queryStatistics = stats
593+
594+
p.handleExchangeResult(d, req, resp, unwrapped)
593595

594596
return resp != nil, err
595597
}

proxy/proxycache.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ func (p *Proxy) replyFromCache(d *DNSContext) (hit bool) {
3838
}
3939

4040
d.Res = ci.m
41-
d.CachedUpstreamAddr = ci.u
41+
d.queryStatistics = cachedQueryStatistics(ci.u)
4242

4343
p.logger.Debug(
4444
"replying from cache",

proxy/stats.go

+177
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package proxy
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
"github.com/AdguardTeam/dnsproxy/upstream"
8+
"github.com/miekg/dns"
9+
)
10+
11+
// upstreamWithStats is a wrapper around the [upstream.Upstream] interface that
12+
// gathers statistics.
13+
type upstreamWithStats struct {
14+
// upstream is the upstream DNS resolver.
15+
upstream upstream.Upstream
16+
17+
// err is the DNS lookup error, if any.
18+
err error
19+
20+
// queryDuration is the duration of the successful DNS lookup.
21+
queryDuration time.Duration
22+
}
23+
24+
// type check
25+
var _ upstream.Upstream = (*upstreamWithStats)(nil)
26+
27+
// Exchange implements the [upstream.Upstream] for *upstreamWithStats.
28+
func (u *upstreamWithStats) Exchange(req *dns.Msg) (resp *dns.Msg, err error) {
29+
start := time.Now()
30+
resp, err = u.upstream.Exchange(req)
31+
u.err = err
32+
u.queryDuration = time.Since(start)
33+
34+
return resp, err
35+
}
36+
37+
// Address implements the [upstream.Upstream] for *upstreamWithStats.
38+
func (u *upstreamWithStats) Address() (addr string) {
39+
return u.upstream.Address()
40+
}
41+
42+
// Close implements the [upstream.Upstream] for *upstreamWithStats.
43+
func (u *upstreamWithStats) Close() (err error) {
44+
return u.upstream.Close()
45+
}
46+
47+
// upstreamsWithStats takes a list of upstreams, wraps each upstream with
48+
// [upstreamWithStats] to gather statistics, and returns the wrapped upstreams.
49+
func upstreamsWithStats(upstreams []upstream.Upstream) (wrapped []upstream.Upstream) {
50+
wrapped = make([]upstream.Upstream, 0, len(upstreams))
51+
for _, u := range upstreams {
52+
wrapped = append(wrapped, &upstreamWithStats{upstream: u})
53+
}
54+
55+
return wrapped
56+
}
57+
58+
// QueryStatistics contains the DNS query statistics for both the upstream and
59+
// fallback DNS servers.
60+
type QueryStatistics struct {
61+
main []*UpstreamStatistics
62+
fallback []*UpstreamStatistics
63+
}
64+
65+
// cachedQueryStatistics returns the DNS query statistics for cached queries.
66+
func cachedQueryStatistics(addr string) (s *QueryStatistics) {
67+
return &QueryStatistics{
68+
main: []*UpstreamStatistics{{
69+
Address: addr,
70+
IsCached: true,
71+
}},
72+
}
73+
}
74+
75+
// Main returns the DNS query statistics for the upstream DNS servers.
76+
func (s *QueryStatistics) Main() (us []*UpstreamStatistics) {
77+
return s.main
78+
}
79+
80+
// Fallback returns the DNS query statistics for the fallback DNS servers.
81+
func (s *QueryStatistics) Fallback() (us []*UpstreamStatistics) {
82+
return s.fallback
83+
}
84+
85+
// collectQueryStats gathers the statistics from the wrapped upstreams.
86+
// resolver is an upstream DNS resolver that successfully resolved the request,
87+
// if any. Provided upstreams must be of type [*upstreamWithStats]. unwrapped
88+
// is the unwrapped resolver, see [upstreamWithStats.upstream]. The returned
89+
// statistics depend on whether the DNS request was successfully resolved and
90+
// the upstream mode, see [DNSContext.QueryStatistics].
91+
func collectQueryStats(
92+
mode UpstreamMode,
93+
resolver upstream.Upstream,
94+
upstreams []upstream.Upstream,
95+
fallbacks []upstream.Upstream,
96+
) (unwrapped upstream.Upstream, stats *QueryStatistics) {
97+
var wrapped *upstreamWithStats
98+
if resolver != nil {
99+
var ok bool
100+
wrapped, ok = resolver.(*upstreamWithStats)
101+
if !ok {
102+
// Should never happen.
103+
panic(fmt.Errorf("unexpected type %T", resolver))
104+
}
105+
106+
unwrapped = wrapped.upstream
107+
}
108+
109+
// The DNS query was not resolved.
110+
if wrapped == nil {
111+
return nil, &QueryStatistics{
112+
main: collectUpstreamStats(upstreams...),
113+
fallback: collectUpstreamStats(fallbacks...),
114+
}
115+
}
116+
117+
// The DNS query was successfully resolved by main resolver and the upstream
118+
// mode is [UpstreamModeFastestAddr].
119+
if mode == UpstreamModeFastestAddr && len(fallbacks) == 0 {
120+
return unwrapped, &QueryStatistics{
121+
main: collectUpstreamStats(upstreams...),
122+
}
123+
}
124+
125+
// The DNS query was resolved by fallback resolver.
126+
if len(fallbacks) > 0 {
127+
return unwrapped, &QueryStatistics{
128+
main: collectUpstreamStats(upstreams...),
129+
fallback: collectUpstreamStats(wrapped),
130+
}
131+
}
132+
133+
// The DNS query was successfully resolved by main resolver.
134+
return unwrapped, &QueryStatistics{
135+
main: collectUpstreamStats(wrapped),
136+
}
137+
}
138+
139+
// UpstreamStatistics contains the DNS query statistics.
140+
type UpstreamStatistics struct {
141+
// Error is the DNS lookup error, if any.
142+
Error error
143+
144+
// Address is the address of the upstream DNS resolver.
145+
//
146+
// TODO(s.chzhen): Use [upstream.Upstream] when [cacheItem] starts to
147+
// contain one.
148+
Address string
149+
150+
// QueryDuration is the duration of the successful DNS lookup.
151+
QueryDuration time.Duration
152+
153+
// IsCached indicates whether the response was served from a cache.
154+
IsCached bool
155+
}
156+
157+
// collectUpstreamStats gathers the upstream statistics from the list of wrapped
158+
// upstreams. upstreams must be of type *upstreamWithStats.
159+
func collectUpstreamStats(upstreams ...upstream.Upstream) (stats []*UpstreamStatistics) {
160+
stats = make([]*UpstreamStatistics, 0, len(upstreams))
161+
162+
for _, u := range upstreams {
163+
w, ok := u.(*upstreamWithStats)
164+
if !ok {
165+
// Should never happen.
166+
panic(fmt.Errorf("unexpected type %T", u))
167+
}
168+
169+
stats = append(stats, &UpstreamStatistics{
170+
Error: w.err,
171+
Address: w.Address(),
172+
QueryDuration: w.queryDuration,
173+
})
174+
}
175+
176+
return stats
177+
}

0 commit comments

Comments
 (0)