Compare commits
20 Commits
master
...
3389-query
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94458c5658 | ||
|
|
6fea435d89 | ||
|
|
05706bd7ea | ||
|
|
00327757e1 | ||
|
|
5f0e53ded7 | ||
|
|
5cd4ce766d | ||
|
|
e695fd9885 | ||
|
|
c43053e7d2 | ||
|
|
86e25944b3 | ||
|
|
fd7260f6de | ||
|
|
c591e46254 | ||
|
|
66d9ea7cca | ||
|
|
dafc785845 | ||
|
|
e9b17891bb | ||
|
|
0b27f048a7 | ||
|
|
649454e77b | ||
|
|
ca22d8524d | ||
|
|
07f4f0474c | ||
|
|
8813e135b6 | ||
|
|
f4f2c11eb9 |
@@ -25,6 +25,9 @@ NOTE: Add new changes BELOW THIS COMMENT.
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- The new HTTP API, `GET /control/querylog/export`, which can be used to
|
||||||
|
export query log items. See `openapi/openapi.yaml` for the full description
|
||||||
|
([#3389]).
|
||||||
- The ability to set inactivity periods for filtering blocked services in the
|
- The ability to set inactivity periods for filtering blocked services in the
|
||||||
configuration file ([#951]). The UI changes are coming in the upcoming
|
configuration file ([#951]). The UI changes are coming in the upcoming
|
||||||
releases.
|
releases.
|
||||||
@@ -89,6 +92,7 @@ In this release, the schema version has changed from 20 to 21.
|
|||||||
|
|
||||||
[#951]: https://github.com/AdguardTeam/AdGuardHome/issues/951
|
[#951]: https://github.com/AdguardTeam/AdGuardHome/issues/951
|
||||||
[#1577]: https://github.com/AdguardTeam/AdGuardHome/issues/1577
|
[#1577]: https://github.com/AdguardTeam/AdGuardHome/issues/1577
|
||||||
|
[#3389]: https://github.com/AdguardTeam/AdGuardHome/issues/3389
|
||||||
[#5910]: https://github.com/AdguardTeam/AdGuardHome/issues/5910
|
[#5910]: https://github.com/AdguardTeam/AdGuardHome/issues/5910
|
||||||
[#5913]: https://github.com/AdguardTeam/AdGuardHome/issues/5913
|
[#5913]: https://github.com/AdguardTeam/AdGuardHome/issues/5913
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ package home
|
|||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghio"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghio"
|
||||||
|
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// middlerware is a wrapper function signature.
|
// middleware is a wrapper function signature.
|
||||||
type middleware func(http.Handler) http.Handler
|
type middleware func(http.Handler) http.Handler
|
||||||
|
|
||||||
// withMiddlewares consequently wraps h with all the middlewares.
|
// withMiddlewares consequently wraps h with all the middlewares.
|
||||||
@@ -75,3 +75,48 @@ func limitRequestBody(h http.Handler) (limited http.Handler) {
|
|||||||
h.ServeHTTP(w, rr)
|
h.ServeHTTP(w, rr)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// defaultWriteTimeout is the maximum duration before timing out writes of
|
||||||
|
// the response.
|
||||||
|
defaultWriteTimeout = 60 * time.Second
|
||||||
|
|
||||||
|
// longerWriteTimeout is the maximum duration before timing out for APIs
|
||||||
|
// expecting longer response requests.
|
||||||
|
longerWriteTimeout = 5 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
// expectsLongTimeoutRequests shows if this request should use a bigger write
|
||||||
|
// timeout value. These are exceptions for poorly designed current APIs as
|
||||||
|
// well as APIs that are designed to expect large files and requests. Remove
|
||||||
|
// once the new, better APIs are up.
|
||||||
|
//
|
||||||
|
// TODO(d.kolyshev): This could be achieved with [http.NewResponseController]
|
||||||
|
// with go v1.20.
|
||||||
|
func expectsLongTimeoutRequests(r *http.Request) (ok bool) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.URL.Path == "/control/querylog/export"
|
||||||
|
}
|
||||||
|
|
||||||
|
// addWriteTimeout wraps underlying handler h, adding a response write timeout.
|
||||||
|
func addWriteTimeout(h http.Handler) (limited http.Handler) {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var handler http.Handler
|
||||||
|
if expectsLongTimeoutRequests(r) {
|
||||||
|
handler = http.TimeoutHandler(h, longerWriteTimeout, "write timeout exceeded")
|
||||||
|
} else {
|
||||||
|
handler = http.TimeoutHandler(h, defaultWriteTimeout, "write timeout exceeded")
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// limitHandler wraps underlying handler h with default limits, such as request
|
||||||
|
// body limit and write timeout.
|
||||||
|
func limitHandler(h http.Handler) (limited http.Handler) {
|
||||||
|
return limitRequestBody(addWriteTimeout(h))
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,11 +25,13 @@ const (
|
|||||||
// readTimeout is the maximum duration for reading the entire request,
|
// readTimeout is the maximum duration for reading the entire request,
|
||||||
// including the body.
|
// including the body.
|
||||||
readTimeout = 60 * time.Second
|
readTimeout = 60 * time.Second
|
||||||
|
|
||||||
// readHdrTimeout is the amount of time allowed to read request headers.
|
// readHdrTimeout is the amount of time allowed to read request headers.
|
||||||
readHdrTimeout = 60 * time.Second
|
readHdrTimeout = 60 * time.Second
|
||||||
|
|
||||||
// writeTimeout is the maximum duration before timing out writes of the
|
// writeTimeout is the maximum duration before timing out writes of the
|
||||||
// response.
|
// response. This limit is overwritten by [addWriteTimeout] middleware.
|
||||||
writeTimeout = 60 * time.Second
|
writeTimeout = 10 * time.Minute
|
||||||
)
|
)
|
||||||
|
|
||||||
type webConfig struct {
|
type webConfig struct {
|
||||||
@@ -169,7 +171,7 @@ func (web *webAPI) start() {
|
|||||||
errs := make(chan error, 2)
|
errs := make(chan error, 2)
|
||||||
|
|
||||||
// Use an h2c handler to support unencrypted HTTP/2, e.g. for proxies.
|
// Use an h2c handler to support unencrypted HTTP/2, e.g. for proxies.
|
||||||
hdlr := h2c.NewHandler(withMiddlewares(Context.mux, limitRequestBody), &http2.Server{})
|
hdlr := h2c.NewHandler(withMiddlewares(Context.mux, limitHandler), &http2.Server{})
|
||||||
|
|
||||||
// Create a new instance, because the Web is not usable after Shutdown.
|
// Create a new instance, because the Web is not usable after Shutdown.
|
||||||
hostStr := web.conf.BindHost.String()
|
hostStr := web.conf.BindHost.String()
|
||||||
@@ -254,7 +256,7 @@ func (web *webAPI) tlsServerLoop() {
|
|||||||
CipherSuites: Context.tlsCipherIDs,
|
CipherSuites: Context.tlsCipherIDs,
|
||||||
MinVersion: tls.VersionTLS12,
|
MinVersion: tls.VersionTLS12,
|
||||||
},
|
},
|
||||||
Handler: withMiddlewares(Context.mux, limitRequestBody),
|
Handler: withMiddlewares(Context.mux, limitHandler),
|
||||||
ReadTimeout: web.conf.ReadTimeout,
|
ReadTimeout: web.conf.ReadTimeout,
|
||||||
ReadHeaderTimeout: web.conf.ReadHeaderTimeout,
|
ReadHeaderTimeout: web.conf.ReadHeaderTimeout,
|
||||||
WriteTimeout: web.conf.WriteTimeout,
|
WriteTimeout: web.conf.WriteTimeout,
|
||||||
@@ -288,7 +290,7 @@ func (web *webAPI) mustStartHTTP3(address string) {
|
|||||||
CipherSuites: Context.tlsCipherIDs,
|
CipherSuites: Context.tlsCipherIDs,
|
||||||
MinVersion: tls.VersionTLS12,
|
MinVersion: tls.VersionTLS12,
|
||||||
},
|
},
|
||||||
Handler: withMiddlewares(Context.mux, limitRequestBody),
|
Handler: withMiddlewares(Context.mux, limitHandler),
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("web: starting http/3 server")
|
log.Debug("web: starting http/3 server")
|
||||||
|
|||||||
108
internal/querylog/csv.go
Normal file
108
internal/querylog/csv.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
package querylog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
// csvRow is an alias type for csv rows.
|
||||||
|
type csvRow = [18]string
|
||||||
|
|
||||||
|
// csvHeaderRow is a slice of strings with column names for CSV header row.
|
||||||
|
var csvHeaderRow = csvRow{
|
||||||
|
"ans_dnssec",
|
||||||
|
"ans_rcode",
|
||||||
|
"ans_type",
|
||||||
|
"ans_value",
|
||||||
|
"cached",
|
||||||
|
"client_ip",
|
||||||
|
"client_id",
|
||||||
|
"ecs",
|
||||||
|
"elapsed",
|
||||||
|
"filter_id",
|
||||||
|
"filter_rule",
|
||||||
|
"proto",
|
||||||
|
"qclass",
|
||||||
|
"qname",
|
||||||
|
"qtype",
|
||||||
|
"reason",
|
||||||
|
"time",
|
||||||
|
"upstream",
|
||||||
|
}
|
||||||
|
|
||||||
|
// toCSV returns a slice of strings with entry fields according to the
|
||||||
|
// csvHeaderRow slice.
|
||||||
|
func (e *logEntry) toCSV() (out *csvRow) {
|
||||||
|
var filterID, filterRule string
|
||||||
|
|
||||||
|
if e.Result.IsFiltered && len(e.Result.Rules) > 0 {
|
||||||
|
rule := e.Result.Rules[0]
|
||||||
|
filterID = strconv.FormatInt(rule.FilterListID, 10)
|
||||||
|
filterRule = rule.Text
|
||||||
|
}
|
||||||
|
|
||||||
|
aData := ansData(e)
|
||||||
|
|
||||||
|
return &csvRow{
|
||||||
|
strconv.FormatBool(e.AuthenticatedData),
|
||||||
|
aData.rCode,
|
||||||
|
aData.typ,
|
||||||
|
aData.value,
|
||||||
|
strconv.FormatBool(e.Cached),
|
||||||
|
e.IP.String(),
|
||||||
|
e.ClientID,
|
||||||
|
e.ReqECS,
|
||||||
|
strconv.FormatFloat(e.Elapsed.Seconds()*1000, 'f', -1, 64),
|
||||||
|
filterID,
|
||||||
|
filterRule,
|
||||||
|
string(e.ClientProto),
|
||||||
|
e.QClass,
|
||||||
|
e.QHost,
|
||||||
|
e.QType,
|
||||||
|
e.Result.Reason.String(),
|
||||||
|
e.Time.Format(time.RFC3339Nano),
|
||||||
|
e.Upstream,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// csvAnswer is a helper struct for csv row answer fields.
|
||||||
|
type csvAnswer struct {
|
||||||
|
rCode string
|
||||||
|
typ string
|
||||||
|
value string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ansData returns a map with message answer data.
|
||||||
|
func ansData(entry *logEntry) (out csvAnswer) {
|
||||||
|
if len(entry.Answer) == 0 {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := &dns.Msg{}
|
||||||
|
if err := msg.Unpack(entry.Answer); err != nil {
|
||||||
|
log.Debug("querylog: failed to unpack dns msg answer: %v: %s", entry.Answer, err)
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
out.rCode = dns.RcodeToString[msg.Rcode]
|
||||||
|
|
||||||
|
if len(msg.Answer) == 0 {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
rr := msg.Answer[0]
|
||||||
|
header := rr.Header()
|
||||||
|
|
||||||
|
out.typ = dns.TypeToString[header.Rrtype]
|
||||||
|
|
||||||
|
// Remove the header string from the answer value since it's mostly
|
||||||
|
// unnecessary in the log.
|
||||||
|
out.value = strings.TrimPrefix(rr.String(), header.String())
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
73
internal/querylog/csv_test.go
Normal file
73
internal/querylog/csv_test.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package querylog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testDate = time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
func TestLogEntry_toCSV(t *testing.T) {
|
||||||
|
ans, err := dns.NewRR("www.example.org. IN A 127.0.0.1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ansBytes, err := (&dns.Msg{Answer: []dns.RR{ans}}).Pack()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
entry *logEntry
|
||||||
|
want *csvRow
|
||||||
|
name string
|
||||||
|
}{{
|
||||||
|
name: "simple",
|
||||||
|
entry: &logEntry{
|
||||||
|
Time: testDate,
|
||||||
|
QHost: "test.host",
|
||||||
|
QType: "A",
|
||||||
|
QClass: "IN",
|
||||||
|
ReqECS: "",
|
||||||
|
ClientID: "test-client-id",
|
||||||
|
ClientProto: ClientProtoDoH,
|
||||||
|
Upstream: "https://test.upstream:443/dns-query",
|
||||||
|
Answer: ansBytes,
|
||||||
|
OrigAnswer: nil,
|
||||||
|
IP: net.IP{1, 2, 3, 4},
|
||||||
|
Result: filtering.Result{},
|
||||||
|
Elapsed: 500 * time.Millisecond,
|
||||||
|
Cached: false,
|
||||||
|
AuthenticatedData: false,
|
||||||
|
},
|
||||||
|
want: &[18]string{
|
||||||
|
"false",
|
||||||
|
"NOERROR",
|
||||||
|
"A",
|
||||||
|
"127.0.0.1",
|
||||||
|
"false",
|
||||||
|
"1.2.3.4",
|
||||||
|
"test-client-id",
|
||||||
|
"",
|
||||||
|
"500",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"doh",
|
||||||
|
"IN",
|
||||||
|
"test.host",
|
||||||
|
"A",
|
||||||
|
"NotFilteredNotFound",
|
||||||
|
"2022-01-01T00:00:00Z",
|
||||||
|
"https://test.upstream:443/dns-query",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tc.want, tc.entry.toCSV())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package querylog
|
package querylog
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/csv"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
@@ -14,6 +15,7 @@ import (
|
|||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||||
|
"github.com/AdguardTeam/golibs/httphdr"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
"github.com/AdguardTeam/golibs/stringutil"
|
"github.com/AdguardTeam/golibs/stringutil"
|
||||||
"github.com/AdguardTeam/golibs/timeutil"
|
"github.com/AdguardTeam/golibs/timeutil"
|
||||||
@@ -62,6 +64,7 @@ func (l *queryLog) initWeb() {
|
|||||||
l.conf.HTTPRegister(http.MethodGet, "/control/querylog", l.handleQueryLog)
|
l.conf.HTTPRegister(http.MethodGet, "/control/querylog", l.handleQueryLog)
|
||||||
l.conf.HTTPRegister(http.MethodPost, "/control/querylog_clear", l.handleQueryLogClear)
|
l.conf.HTTPRegister(http.MethodPost, "/control/querylog_clear", l.handleQueryLogClear)
|
||||||
l.conf.HTTPRegister(http.MethodGet, "/control/querylog/config", l.handleGetQueryLogConfig)
|
l.conf.HTTPRegister(http.MethodGet, "/control/querylog/config", l.handleGetQueryLogConfig)
|
||||||
|
l.conf.HTTPRegister(http.MethodGet, "/control/querylog/export", l.handleQueryLogExport)
|
||||||
l.conf.HTTPRegister(
|
l.conf.HTTPRegister(
|
||||||
http.MethodPut,
|
http.MethodPut,
|
||||||
"/control/querylog/config/update",
|
"/control/querylog/config/update",
|
||||||
@@ -96,6 +99,73 @@ func (l *queryLog) handleQueryLog(w http.ResponseWriter, r *http.Request) {
|
|||||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// exportChunkSize is a size of one search-flush iteration for query log export.
|
||||||
|
//
|
||||||
|
// TODO(a.meshkov): Consider making configurable.
|
||||||
|
const exportChunkSize = 500
|
||||||
|
|
||||||
|
// handleQueryLogExport is the handler for the GET /control/querylog/export
|
||||||
|
// HTTP API.
|
||||||
|
func (l *queryLog) handleQueryLogExport(w http.ResponseWriter, r *http.Request) {
|
||||||
|
searchCriteria, err := parseSearchCriteria(r.URL.Query())
|
||||||
|
if err != nil {
|
||||||
|
aghhttp.Error(r, w, http.StatusBadRequest, "parsing params: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
params := &searchParams{
|
||||||
|
limit: exportChunkSize,
|
||||||
|
searchCriteria: searchCriteria,
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set(httphdr.ContentType, "text/csv; charset=UTF-8; header=present")
|
||||||
|
w.Header().Set(httphdr.ContentDisposition, "attachment;filename=data.csv")
|
||||||
|
|
||||||
|
csvWriter := csv.NewWriter(w)
|
||||||
|
|
||||||
|
// Write header.
|
||||||
|
if err = csvWriter.Write(csvHeaderRow[:]); err != nil {
|
||||||
|
http.Error(w, "writing csv header", http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
csvWriter.Flush()
|
||||||
|
|
||||||
|
var entries []*logEntry
|
||||||
|
for {
|
||||||
|
func() {
|
||||||
|
l.confMu.RLock()
|
||||||
|
defer l.confMu.RUnlock()
|
||||||
|
|
||||||
|
entries, _ = l.search(params)
|
||||||
|
}()
|
||||||
|
|
||||||
|
if len(entries) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
params.offset += params.limit
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
row := entry.toCSV()
|
||||||
|
if err = csvWriter.Write(row[:]); err != nil {
|
||||||
|
// TODO(a.garipov): Set Trailer X-Error header.
|
||||||
|
log.Error("%s %s %s: %s: %s", r.Method, r.Host, r.URL, "writing csv record", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
csvWriter.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = csvWriter.Error(); err != nil {
|
||||||
|
// TODO(a.garipov): Set Trailer X-Error header.
|
||||||
|
log.Error("%s %s %s: %s: %s", r.Method, r.Host, r.URL, "writing csv", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// handleQueryLogClear is the handler for the POST /control/querylog/clear HTTP
|
// handleQueryLogClear is the handler for the POST /control/querylog/clear HTTP
|
||||||
// API.
|
// API.
|
||||||
func (l *queryLog) handleQueryLogClear(_ http.ResponseWriter, _ *http.Request) {
|
func (l *queryLog) handleQueryLogClear(_ http.ResponseWriter, _ *http.Request) {
|
||||||
@@ -360,6 +430,17 @@ func parseSearchParams(r *http.Request) (p *searchParams, err error) {
|
|||||||
p.maxFileScanEntries = 0
|
p.maxFileScanEntries = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p.searchCriteria, err = parseSearchCriteria(q)
|
||||||
|
if err != nil {
|
||||||
|
// Don't wrap the error, because it's informative enough as is.
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSearchCriteria parses a list of search criteria from the query.
|
||||||
|
func parseSearchCriteria(q url.Values) (searchCriteria []searchCriterion, err error) {
|
||||||
for _, v := range []struct {
|
for _, v := range []struct {
|
||||||
urlField string
|
urlField string
|
||||||
ct criterionType
|
ct criterionType
|
||||||
@@ -378,9 +459,9 @@ func parseSearchParams(r *http.Request) (p *searchParams, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ok {
|
if ok {
|
||||||
p.searchCriteria = append(p.searchCriteria, c)
|
searchCriteria = append(searchCriteria, c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return p, nil
|
return searchCriteria, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,3 +93,67 @@ func TestQueryLog_Search_findClient(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(t, knownClientName, gotClient.Name)
|
assert.Equal(t, knownClientName, gotClient.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BenchmarkQueryLog_Search compares the speed of search with limit-offset
|
||||||
|
// parameters and the one with oldenThan timestamp specified.
|
||||||
|
func BenchmarkQueryLog_Search(b *testing.B) {
|
||||||
|
l, err := newQueryLog(Config{
|
||||||
|
Enabled: true,
|
||||||
|
RotationIvl: timeutil.Day,
|
||||||
|
MemSize: 100,
|
||||||
|
BaseDir: b.TempDir(),
|
||||||
|
})
|
||||||
|
require.NoError(b, err)
|
||||||
|
|
||||||
|
const (
|
||||||
|
entNum = 100000
|
||||||
|
firstPageDomain = "first.example.org"
|
||||||
|
secondPageDomain = "second.example.org"
|
||||||
|
)
|
||||||
|
// Add entries to the log.
|
||||||
|
for i := 0; i < entNum; i++ {
|
||||||
|
addEntry(l, secondPageDomain, net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
|
||||||
|
}
|
||||||
|
// Write them to the first file.
|
||||||
|
require.NoError(b, l.flushLogBuffer())
|
||||||
|
|
||||||
|
// Add more to the in-memory part of log.
|
||||||
|
for i := 0; i < entNum; i++ {
|
||||||
|
addEntry(l, firstPageDomain, net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Run("limit_offset", func(b *testing.B) {
|
||||||
|
params := newSearchParams()
|
||||||
|
|
||||||
|
b.ReportAllocs()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
params.offset += params.limit
|
||||||
|
_, _ = l.search(params)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
b.Run("timestamp", func(b *testing.B) {
|
||||||
|
params := newSearchParams()
|
||||||
|
params.olderThan = time.Now().Add(-1 * time.Hour)
|
||||||
|
|
||||||
|
b.ReportAllocs()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
params.olderThan = params.olderThan.Add(1 * time.Minute)
|
||||||
|
_, _ = l.search(params)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Most recent result, on a MBP15:
|
||||||
|
//
|
||||||
|
// goos: darwin
|
||||||
|
// goarch: amd64
|
||||||
|
// pkg: github.com/AdguardTeam/AdGuardHome/internal/querylog
|
||||||
|
// cpu: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
|
||||||
|
// BenchmarkQueryLog_Search
|
||||||
|
// BenchmarkQueryLog_Search/limit_offset
|
||||||
|
// BenchmarkQueryLog_Search/limit_offset-12 547 2066079 ns/op 2325019 B/op 26633 allocs/op
|
||||||
|
// BenchmarkQueryLog_Search/timestamp
|
||||||
|
// BenchmarkQueryLog_Search/timestamp-12 1303 2028888 ns/op 2219337 B/op 25194 allocs/op
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,17 @@
|
|||||||
|
|
||||||
## v0.107.30: API changes
|
## v0.107.30: API changes
|
||||||
|
|
||||||
|
### New HTTP API 'GET /control/querylog/export'
|
||||||
|
|
||||||
|
* The new `GET /control/querylog/export` HTTP API allows an export of query log
|
||||||
|
items in the CSV file. It returns a CSV object with the following format:
|
||||||
|
|
||||||
|
```csv
|
||||||
|
ans_dnssec,ans_rcode,ans_type,ans_value,cached,client_ip,client_id,ecs,elapsed,filter_id,filter_rule,proto,qclass,qname,qtype,reason,time,upstream
|
||||||
|
false,NOERROR,A,192.168.1.1,false,127.0.0.1,,,0.097409,,,,IN,example.com,A,Rewrite,2023-01-30T12:21:13.947563+07:00,
|
||||||
|
false,NOERROR,A,45.33.2.79,false,127.0.0.1,,,482.967871,,,,IN,test.com,A,NotFilteredNotFound,2022-12-13T12:18:04.964403+07:00,https://dns10.quad9.net:443/dns-query
|
||||||
|
```
|
||||||
|
|
||||||
### `POST /control/version.json` and `GET /control/dhcp/interfaces` content type
|
### `POST /control/version.json` and `GET /control/dhcp/interfaces` content type
|
||||||
|
|
||||||
* The value of the `Content-Type` header in the `POST /control/version.json` and
|
* The value of the `Content-Type` header in the `POST /control/version.json` and
|
||||||
|
|||||||
@@ -313,6 +313,51 @@
|
|||||||
'responses':
|
'responses':
|
||||||
'200':
|
'200':
|
||||||
'description': 'OK.'
|
'description': 'OK.'
|
||||||
|
'/querylog/export':
|
||||||
|
'get':
|
||||||
|
'tags':
|
||||||
|
- 'log'
|
||||||
|
'description': >
|
||||||
|
Returns a CSV file stream with the following fields, sorted a-z:
|
||||||
|
ans_dnssec, ans_rcode, ans_type, ans_value, cached, client_ip,
|
||||||
|
clientid, ecs, elapsed, filter_id, filter_rule, proto, qclass, qname,
|
||||||
|
qtype, reason, time, upstream. The fields list is a subject to change.
|
||||||
|
The content is UTF-8 encoded with quotation marks.
|
||||||
|
|
||||||
|
'operationId': 'getQueryLogExport'
|
||||||
|
'summary': 'Get DNS server query log items in a CSV stream.'
|
||||||
|
'parameters':
|
||||||
|
- 'name': 'search'
|
||||||
|
'in': 'query'
|
||||||
|
'description': 'Filter by domain name or client IP'
|
||||||
|
'schema':
|
||||||
|
'type': 'string'
|
||||||
|
- 'name': 'response_status'
|
||||||
|
'in': 'query'
|
||||||
|
'description': 'Filter by response status'
|
||||||
|
'schema':
|
||||||
|
'type': 'string'
|
||||||
|
'enum':
|
||||||
|
- 'all'
|
||||||
|
- 'filtered'
|
||||||
|
- 'blocked'
|
||||||
|
- 'blocked_safebrowsing'
|
||||||
|
- 'blocked_parental'
|
||||||
|
- 'whitelisted'
|
||||||
|
- 'rewritten'
|
||||||
|
- 'safe_search'
|
||||||
|
- 'processed'
|
||||||
|
'responses':
|
||||||
|
'200':
|
||||||
|
'description': 'OK.'
|
||||||
|
'content':
|
||||||
|
'text/csv':
|
||||||
|
'schema':
|
||||||
|
'type': 'string'
|
||||||
|
'example': >
|
||||||
|
ans_dnssec,ans_rcode,ans_type,ans_value,cached,client_ip,client_id,ecs,elapsed,filter_id,filter_rule,proto,qclass,qname,qtype,reason,time,upstream
|
||||||
|
false,NOERROR,A,192.168.1.1,false,127.0.0.1,,,0.097409,,,,IN,example.com,A,Rewrite,2023-01-30T12:21:13.947563+07:00,
|
||||||
|
false,NOERROR,A,45.33.2.79,false,127.0.0.1,,,482.967871,,,,IN,test.com,A,NotFilteredNotFound,2022-12-13T12:18:04.964403+07:00,https://dns10.quad9.net:443/dns-query
|
||||||
'/stats':
|
'/stats':
|
||||||
'get':
|
'get':
|
||||||
'tags':
|
'tags':
|
||||||
|
|||||||
Reference in New Issue
Block a user