178 lines
4.6 KiB
Go
Raw Normal View History

2022-08-26 14:18:35 +03:00
package querylog
import (
"bytes"
"context"
"encoding/json"
"fmt"
2024-10-14 17:44:24 +03:00
"log/slog"
"math"
2024-01-04 19:22:32 +03:00
"net/netip"
2022-08-26 14:18:35 +03:00
"os"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/metrics"
2024-10-14 17:44:24 +03:00
"github.com/AdguardTeam/AdGuardDNS/internal/optslog"
2022-08-26 14:18:35 +03:00
"github.com/AdguardTeam/golibs/errors"
2024-10-14 17:44:24 +03:00
"github.com/AdguardTeam/golibs/logutil/slogutil"
2023-02-03 15:27:58 +03:00
"github.com/AdguardTeam/golibs/mathutil"
2024-01-04 19:22:32 +03:00
"github.com/AdguardTeam/golibs/syncutil"
2024-06-07 14:27:46 +03:00
"golang.org/x/exp/rand"
2022-08-26 14:18:35 +03:00
)
2024-10-14 17:44:24 +03:00
// FileSystemConfig is the configuration of the file system query log. All
// fields must not be empty.
2022-08-26 14:18:35 +03:00
type FileSystemConfig struct {
2024-10-14 17:44:24 +03:00
// Logger is used for debug logging.
Logger *slog.Logger
2022-08-26 14:18:35 +03:00
// Path is the path to the log file.
Path string
2024-06-07 14:27:46 +03:00
// RandSeed is used to set the "rn" property in JSON objects.
RandSeed uint64
2022-08-26 14:18:35 +03:00
}
// NewFileSystem creates a new file system query log. The log is safe for
2024-06-07 14:27:46 +03:00
// concurrent use. c must not be nil.
2022-08-26 14:18:35 +03:00
func NewFileSystem(c *FileSystemConfig) (l *FileSystem) {
2024-06-07 14:27:46 +03:00
rng := rand.New(&rand.LockedSource{})
rng.Seed(c.RandSeed)
2022-08-26 14:18:35 +03:00
return &FileSystem{
2024-10-14 17:44:24 +03:00
logger: c.Logger,
2024-01-04 19:22:32 +03:00
bufferPool: syncutil.NewPool(func() (v *entryBuffer) {
2023-09-06 08:22:07 +03:00
return &entryBuffer{
ent: &jsonlEntry{},
buf: &bytes.Buffer{},
}
}),
2024-06-07 14:27:46 +03:00
rng: rng,
path: c.Path,
2022-08-26 14:18:35 +03:00
}
}
// entryBuffer is a struct with two fields for caching entry that is being
2024-06-07 14:27:46 +03:00
// written. Using this struct allows us to remove allocations on every write.
2022-08-26 14:18:35 +03:00
type entryBuffer struct {
ent *jsonlEntry
buf *bytes.Buffer
}
// FileSystem is the file system implementation of the AdGuard DNS query log.
type FileSystem struct {
2024-10-14 17:44:24 +03:00
// logger is used for debug logging.
logger *slog.Logger
2023-09-06 08:22:07 +03:00
// bufferPool is a pool with [*entryBuffer] instances used to avoid extra
// allocations when serializing query log items to JSON and writing them.
2024-01-04 19:22:32 +03:00
bufferPool *syncutil.Pool[entryBuffer]
2022-08-26 14:18:35 +03:00
2024-06-07 14:27:46 +03:00
// rng is used to generate random numbers for the "rn" property in the
// resulting JSON.
rng *rand.Rand
2022-08-26 14:18:35 +03:00
// path is the path to the query log file.
path string
}
// type check
var _ Interface = (*FileSystem)(nil)
// Write implements the Interface interface for *FileSystem.
2024-10-14 17:44:24 +03:00
func (l *FileSystem) Write(ctx context.Context, e *Entry) (err error) {
optslog.Trace1(ctx, l.logger, "writing file logs", "req_id", e.RequestID)
2022-08-26 14:18:35 +03:00
defer func() {
2024-10-14 17:44:24 +03:00
optslog.Trace2(
ctx,
l.logger,
"writing file logs",
"req_id", e.RequestID,
slogutil.KeyError, err,
)
2022-08-26 14:18:35 +03:00
}()
startTime := time.Now()
defer func() {
metrics.QueryLogWriteDuration.Observe(time.Since(startTime).Seconds())
metrics.QueryLogItemsCount.Inc()
}()
2023-09-06 08:22:07 +03:00
entBuf := l.bufferPool.Get()
2022-08-26 14:18:35 +03:00
defer l.bufferPool.Put(entBuf)
entBuf.buf.Reset()
2024-01-04 19:22:32 +03:00
var remoteIP *netip.Addr
if e.RemoteIP != (netip.Addr{}) {
remoteIP = &e.RemoteIP
}
2022-08-26 14:18:35 +03:00
c, id, r := resultData(e.RequestResult, e.ResponseResult)
*entBuf.ent = jsonlEntry{
2023-08-08 18:31:48 +03:00
RequestID: e.RequestID.String(),
2022-08-26 14:18:35 +03:00
ProfileID: e.ProfileID,
DeviceID: e.DeviceID,
ClientCountry: e.ClientCountry,
ResponseCountry: e.ResponseCountry,
DomainFQDN: e.DomainFQDN,
FilterListID: id,
FilterRule: r,
Timestamp: e.Time.UnixMilli(),
ClientASN: e.ClientASN,
2024-10-14 17:44:24 +03:00
Elapsed: l.convertElapsed(ctx, e.Elapsed),
2022-08-26 14:18:35 +03:00
RequestType: e.RequestType,
ResponseCode: e.ResponseCode,
2024-10-14 17:44:24 +03:00
// #nosec G115 -- The overflow is safe, since this is a random number.
Random: uint16(l.rng.Uint32()),
DNSSEC: mathutil.BoolToNumber[uint8](e.DNSSEC),
Protocol: e.Protocol,
ResultCode: c,
RemoteIP: remoteIP,
2022-08-26 14:18:35 +03:00
}
var f *os.File
f, err = os.OpenFile(l.path, agd.DefaultWOFlags, agd.DefaultPerm)
if err != nil {
return fmt.Errorf("opening query log file: %w", err)
}
defer func() { err = errors.WithDeferred(err, f.Close()) }()
// Serialize the query log entry to that buffer as a JSON.
// Do not write an additional line feed, because Encode already does that.
err = json.NewEncoder(entBuf.buf).Encode(entBuf.ent)
if err != nil {
return fmt.Errorf("writing log: %w", err)
}
var written int64
written, err = entBuf.buf.WriteTo(f)
if err != nil {
return fmt.Errorf("writing log: %w", err)
}
metrics.QueryLogItemSize.Observe(float64(written))
return nil
}
2024-10-14 17:44:24 +03:00
// convertElapsed converts the elapsed duration and writes warnings to the log
// if the value is outside of the allowed limits.
func (l *FileSystem) convertElapsed(ctx context.Context, elapsed time.Duration) (elapsedMs uint32) {
elapsedMs64 := elapsed.Milliseconds()
if elapsedMs64 < 0 {
l.logger.WarnContext(ctx, "elapsed below zero; setting to zero")
return 0
}
const maxElapsedMs = math.MaxUint32
if elapsedMs64 > maxElapsedMs {
l.logger.WarnContext(ctx, "elapsed above max uint32; setting to max uint32")
return maxElapsedMs
}
return uint32(elapsedMs64)
}