Skip to content

Commit 4f686ad

Browse files
committed
proxy: query statistics
1 parent 429c98c commit 4f686ad

File tree

4 files changed

+247
-19
lines changed

4 files changed

+247
-19
lines changed

proxy/dnscontext.go

Lines changed: 16 additions & 9 deletions
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,16 @@ 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.
117+
func (dctx *DNSContext) QueryStatistics() (s *QueryStatistics) {
118+
if dctx == nil {
119+
return nil
120+
}
121+
122+
return dctx.queryStatistics
123+
}
124+
118125
// calcFlagsAndSize lazily calculates some values required for Resolve method.
119126
func (dctx *DNSContext) calcFlagsAndSize() {
120127
if dctx.udpSize != 0 || dctx.Req == nil {

proxy/proxy.go

Lines changed: 11 additions & 9 deletions
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, false)
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, true)
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

Lines changed: 1 addition & 1 deletion
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

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

0 commit comments

Comments
 (0)