blocky/resolver/query_logging_resolver.go

192 lines
5.0 KiB
Go
Raw Normal View History

2020-01-12 18:23:35 +01:00
package resolver
import (
"context"
2020-01-12 18:23:35 +01:00
"time"
2021-08-25 22:06:34 +02:00
"github.com/0xERR0R/blocky/config"
"github.com/0xERR0R/blocky/log"
"github.com/0xERR0R/blocky/model"
"github.com/0xERR0R/blocky/querylog"
"github.com/0xERR0R/blocky/util"
2021-12-24 22:40:06 +01:00
"github.com/avast/retry-go/v4"
"github.com/miekg/dns"
2020-01-12 18:23:35 +01:00
)
const (
refactor: configuration rework (usage and printing) (#920) * refactor: make `config.Duration` a struct with `time.Duration` embed Allows directly calling `time.Duration` methods. * refactor(HostsFileResolver): don't copy individual config items The idea is to make adding configuration options easier, and searching for references straight forward. * refactor: move config printing to struct and use a logger Using a logger allows using multiple levels so the whole configuration can be printed in trace/verbose mode, but only important parts are shown by default. * squash: rename `Cast` to `ToDuration` * squash: revert `Duration` to a simple wrapper ("new type" pattern) * squash: `Duration.IsZero` tests * squash: refactor resolvers to rely on their config directly if possible * squash: implement `IsEnabled` and `LogValues` for all resolvers * refactor: use go-enum `--values` to simplify getting all log fields * refactor: simplify `QType` unmarshaling * squash: rename `ValueLogger` to `Configurable` * squash: rename `UpstreamConfig` to `ParallelBestConfig` * squash: rename `RewriteConfig` to `RewriterConfig` * squash: config tests * squash: resolver tests * squash: add `ForEach` test and improve `Chain` ones * squash: simplify implementing `config.Configurable` * squash: minor changes for better coverage * squash: more `UnmarshalYAML` -> `UnmarshalText` * refactor: move `config.Upstream` into own file * refactor: add `Resolver.Type` method * squash: add `log` method to `typed` to use `Resolover.Type` as prefix * squash: tweak startup config logging * squash: add `LogResolverConfig` tests * squash: make sure all options of type `Duration` use `%s`
2023-03-12 22:14:10 +01:00
cleanUpRunPeriod = 12 * time.Hour
queryLoggingResolverType = "query_logging"
logChanCap = 1000
2020-01-12 18:23:35 +01:00
)
2021-09-12 22:26:07 +02:00
// QueryLoggingResolver writes query information (question, answer, duration, ...)
2020-01-12 18:23:35 +01:00
type QueryLoggingResolver struct {
configurable[*config.QueryLog]
2020-01-12 18:23:35 +01:00
NextResolver
refactor: configuration rework (usage and printing) (#920) * refactor: make `config.Duration` a struct with `time.Duration` embed Allows directly calling `time.Duration` methods. * refactor(HostsFileResolver): don't copy individual config items The idea is to make adding configuration options easier, and searching for references straight forward. * refactor: move config printing to struct and use a logger Using a logger allows using multiple levels so the whole configuration can be printed in trace/verbose mode, but only important parts are shown by default. * squash: rename `Cast` to `ToDuration` * squash: revert `Duration` to a simple wrapper ("new type" pattern) * squash: `Duration.IsZero` tests * squash: refactor resolvers to rely on their config directly if possible * squash: implement `IsEnabled` and `LogValues` for all resolvers * refactor: use go-enum `--values` to simplify getting all log fields * refactor: simplify `QType` unmarshaling * squash: rename `ValueLogger` to `Configurable` * squash: rename `UpstreamConfig` to `ParallelBestConfig` * squash: rename `RewriteConfig` to `RewriterConfig` * squash: config tests * squash: resolver tests * squash: add `ForEach` test and improve `Chain` ones * squash: simplify implementing `config.Configurable` * squash: minor changes for better coverage * squash: more `UnmarshalYAML` -> `UnmarshalText` * refactor: move `config.Upstream` into own file * refactor: add `Resolver.Type` method * squash: add `log` method to `typed` to use `Resolover.Type` as prefix * squash: tweak startup config logging * squash: add `LogResolverConfig` tests * squash: make sure all options of type `Duration` use `%s`
2023-03-12 22:14:10 +01:00
typed
logChan chan *querylog.LogEntry
writer querylog.Writer
2020-01-12 18:23:35 +01:00
}
// NewQueryLoggingResolver returns a new resolver instance
func NewQueryLoggingResolver(ctx context.Context, cfg config.QueryLog) *QueryLoggingResolver {
refactor: configuration rework (usage and printing) (#920) * refactor: make `config.Duration` a struct with `time.Duration` embed Allows directly calling `time.Duration` methods. * refactor(HostsFileResolver): don't copy individual config items The idea is to make adding configuration options easier, and searching for references straight forward. * refactor: move config printing to struct and use a logger Using a logger allows using multiple levels so the whole configuration can be printed in trace/verbose mode, but only important parts are shown by default. * squash: rename `Cast` to `ToDuration` * squash: revert `Duration` to a simple wrapper ("new type" pattern) * squash: `Duration.IsZero` tests * squash: refactor resolvers to rely on their config directly if possible * squash: implement `IsEnabled` and `LogValues` for all resolvers * refactor: use go-enum `--values` to simplify getting all log fields * refactor: simplify `QType` unmarshaling * squash: rename `ValueLogger` to `Configurable` * squash: rename `UpstreamConfig` to `ParallelBestConfig` * squash: rename `RewriteConfig` to `RewriterConfig` * squash: config tests * squash: resolver tests * squash: add `ForEach` test and improve `Chain` ones * squash: simplify implementing `config.Configurable` * squash: minor changes for better coverage * squash: more `UnmarshalYAML` -> `UnmarshalText` * refactor: move `config.Upstream` into own file * refactor: add `Resolver.Type` method * squash: add `log` method to `typed` to use `Resolover.Type` as prefix * squash: tweak startup config logging * squash: add `LogResolverConfig` tests * squash: make sure all options of type `Duration` use `%s`
2023-03-12 22:14:10 +01:00
logger := log.PrefixedLog(queryLoggingResolverType)
var writer querylog.Writer
err := retry.Do(
func() error {
var err error
refactor: configuration rework (usage and printing) (#920) * refactor: make `config.Duration` a struct with `time.Duration` embed Allows directly calling `time.Duration` methods. * refactor(HostsFileResolver): don't copy individual config items The idea is to make adding configuration options easier, and searching for references straight forward. * refactor: move config printing to struct and use a logger Using a logger allows using multiple levels so the whole configuration can be printed in trace/verbose mode, but only important parts are shown by default. * squash: rename `Cast` to `ToDuration` * squash: revert `Duration` to a simple wrapper ("new type" pattern) * squash: `Duration.IsZero` tests * squash: refactor resolvers to rely on their config directly if possible * squash: implement `IsEnabled` and `LogValues` for all resolvers * refactor: use go-enum `--values` to simplify getting all log fields * refactor: simplify `QType` unmarshaling * squash: rename `ValueLogger` to `Configurable` * squash: rename `UpstreamConfig` to `ParallelBestConfig` * squash: rename `RewriteConfig` to `RewriterConfig` * squash: config tests * squash: resolver tests * squash: add `ForEach` test and improve `Chain` ones * squash: simplify implementing `config.Configurable` * squash: minor changes for better coverage * squash: more `UnmarshalYAML` -> `UnmarshalText` * refactor: move `config.Upstream` into own file * refactor: add `Resolver.Type` method * squash: add `log` method to `typed` to use `Resolover.Type` as prefix * squash: tweak startup config logging * squash: add `LogResolverConfig` tests * squash: make sure all options of type `Duration` use `%s`
2023-03-12 22:14:10 +01:00
switch cfg.Type {
case config.QueryLogTypeCsv:
writer, err = querylog.NewCSVWriter(cfg.Target, false, cfg.LogRetentionDays)
case config.QueryLogTypeCsvClient:
writer, err = querylog.NewCSVWriter(cfg.Target, true, cfg.LogRetentionDays)
case config.QueryLogTypeMysql:
writer, err = querylog.NewDatabaseWriter(ctx, "mysql", cfg.Target, cfg.LogRetentionDays,
cfg.FlushInterval.ToDuration())
case config.QueryLogTypePostgresql:
writer, err = querylog.NewDatabaseWriter(ctx, "postgresql", cfg.Target, cfg.LogRetentionDays,
cfg.FlushInterval.ToDuration())
case config.QueryLogTypeConsole:
writer = querylog.NewLoggerWriter()
case config.QueryLogTypeNone:
writer = querylog.NewNoneWriter()
}
return err
},
retry.Attempts(uint(cfg.CreationAttempts)),
retry.DelayType(retry.FixedDelay),
refactor: configuration rework (usage and printing) (#920) * refactor: make `config.Duration` a struct with `time.Duration` embed Allows directly calling `time.Duration` methods. * refactor(HostsFileResolver): don't copy individual config items The idea is to make adding configuration options easier, and searching for references straight forward. * refactor: move config printing to struct and use a logger Using a logger allows using multiple levels so the whole configuration can be printed in trace/verbose mode, but only important parts are shown by default. * squash: rename `Cast` to `ToDuration` * squash: revert `Duration` to a simple wrapper ("new type" pattern) * squash: `Duration.IsZero` tests * squash: refactor resolvers to rely on their config directly if possible * squash: implement `IsEnabled` and `LogValues` for all resolvers * refactor: use go-enum `--values` to simplify getting all log fields * refactor: simplify `QType` unmarshaling * squash: rename `ValueLogger` to `Configurable` * squash: rename `UpstreamConfig` to `ParallelBestConfig` * squash: rename `RewriteConfig` to `RewriterConfig` * squash: config tests * squash: resolver tests * squash: add `ForEach` test and improve `Chain` ones * squash: simplify implementing `config.Configurable` * squash: minor changes for better coverage * squash: more `UnmarshalYAML` -> `UnmarshalText` * refactor: move `config.Upstream` into own file * refactor: add `Resolver.Type` method * squash: add `log` method to `typed` to use `Resolover.Type` as prefix * squash: tweak startup config logging * squash: add `LogResolverConfig` tests * squash: make sure all options of type `Duration` use `%s`
2023-03-12 22:14:10 +01:00
retry.Delay(cfg.CreationCooldown.ToDuration()),
retry.OnRetry(func(n uint, err error) {
logger.Warnf(
"Error occurred on query writer creation, retry attempt %d/%d: %v", n+1, cfg.CreationAttempts, err,
)
}))
if err != nil {
logger.Error("can't create query log writer, using console as fallback: ", err)
writer = querylog.NewLoggerWriter()
refactor: configuration rework (usage and printing) (#920) * refactor: make `config.Duration` a struct with `time.Duration` embed Allows directly calling `time.Duration` methods. * refactor(HostsFileResolver): don't copy individual config items The idea is to make adding configuration options easier, and searching for references straight forward. * refactor: move config printing to struct and use a logger Using a logger allows using multiple levels so the whole configuration can be printed in trace/verbose mode, but only important parts are shown by default. * squash: rename `Cast` to `ToDuration` * squash: revert `Duration` to a simple wrapper ("new type" pattern) * squash: `Duration.IsZero` tests * squash: refactor resolvers to rely on their config directly if possible * squash: implement `IsEnabled` and `LogValues` for all resolvers * refactor: use go-enum `--values` to simplify getting all log fields * refactor: simplify `QType` unmarshaling * squash: rename `ValueLogger` to `Configurable` * squash: rename `UpstreamConfig` to `ParallelBestConfig` * squash: rename `RewriteConfig` to `RewriterConfig` * squash: config tests * squash: resolver tests * squash: add `ForEach` test and improve `Chain` ones * squash: simplify implementing `config.Configurable` * squash: minor changes for better coverage * squash: more `UnmarshalYAML` -> `UnmarshalText` * refactor: move `config.Upstream` into own file * refactor: add `Resolver.Type` method * squash: add `log` method to `typed` to use `Resolover.Type` as prefix * squash: tweak startup config logging * squash: add `LogResolverConfig` tests * squash: make sure all options of type `Duration` use `%s`
2023-03-12 22:14:10 +01:00
cfg.Type = config.QueryLogTypeConsole
}
2022-03-03 11:27:27 +01:00
logChan := make(chan *querylog.LogEntry, logChanCap)
2020-01-12 18:23:35 +01:00
resolver := QueryLoggingResolver{
refactor: configuration rework (usage and printing) (#920) * refactor: make `config.Duration` a struct with `time.Duration` embed Allows directly calling `time.Duration` methods. * refactor(HostsFileResolver): don't copy individual config items The idea is to make adding configuration options easier, and searching for references straight forward. * refactor: move config printing to struct and use a logger Using a logger allows using multiple levels so the whole configuration can be printed in trace/verbose mode, but only important parts are shown by default. * squash: rename `Cast` to `ToDuration` * squash: revert `Duration` to a simple wrapper ("new type" pattern) * squash: `Duration.IsZero` tests * squash: refactor resolvers to rely on their config directly if possible * squash: implement `IsEnabled` and `LogValues` for all resolvers * refactor: use go-enum `--values` to simplify getting all log fields * refactor: simplify `QType` unmarshaling * squash: rename `ValueLogger` to `Configurable` * squash: rename `UpstreamConfig` to `ParallelBestConfig` * squash: rename `RewriteConfig` to `RewriterConfig` * squash: config tests * squash: resolver tests * squash: add `ForEach` test and improve `Chain` ones * squash: simplify implementing `config.Configurable` * squash: minor changes for better coverage * squash: more `UnmarshalYAML` -> `UnmarshalText` * refactor: move `config.Upstream` into own file * refactor: add `Resolver.Type` method * squash: add `log` method to `typed` to use `Resolover.Type` as prefix * squash: tweak startup config logging * squash: add `LogResolverConfig` tests * squash: make sure all options of type `Duration` use `%s`
2023-03-12 22:14:10 +01:00
configurable: withConfig(&cfg),
typed: withType(queryLoggingResolverType),
logChan: logChan,
writer: writer,
2020-01-12 18:23:35 +01:00
}
go resolver.writeLog(ctx)
2020-01-12 18:23:35 +01:00
if cfg.LogRetentionDays > 0 {
go resolver.periodicCleanUp(ctx)
2020-01-12 18:23:35 +01:00
}
return &resolver
}
// triggers periodically cleanup of old log files
func (r *QueryLoggingResolver) periodicCleanUp(ctx context.Context) {
2020-01-12 18:23:35 +01:00
ticker := time.NewTicker(cleanUpRunPeriod)
defer ticker.Stop()
for {
select {
case <-ticker.C:
r.doCleanUp()
case <-ctx.Done():
return
}
2020-01-12 18:23:35 +01:00
}
}
func (r *QueryLoggingResolver) doCleanUp() {
r.writer.CleanUp()
2020-01-12 18:23:35 +01:00
}
// Resolve logs the query, duration and the result
func (r *QueryLoggingResolver) Resolve(ctx context.Context, request *model.Request) (*model.Response, error) {
refactor: configuration rework (usage and printing) (#920) * refactor: make `config.Duration` a struct with `time.Duration` embed Allows directly calling `time.Duration` methods. * refactor(HostsFileResolver): don't copy individual config items The idea is to make adding configuration options easier, and searching for references straight forward. * refactor: move config printing to struct and use a logger Using a logger allows using multiple levels so the whole configuration can be printed in trace/verbose mode, but only important parts are shown by default. * squash: rename `Cast` to `ToDuration` * squash: revert `Duration` to a simple wrapper ("new type" pattern) * squash: `Duration.IsZero` tests * squash: refactor resolvers to rely on their config directly if possible * squash: implement `IsEnabled` and `LogValues` for all resolvers * refactor: use go-enum `--values` to simplify getting all log fields * refactor: simplify `QType` unmarshaling * squash: rename `ValueLogger` to `Configurable` * squash: rename `UpstreamConfig` to `ParallelBestConfig` * squash: rename `RewriteConfig` to `RewriterConfig` * squash: config tests * squash: resolver tests * squash: add `ForEach` test and improve `Chain` ones * squash: simplify implementing `config.Configurable` * squash: minor changes for better coverage * squash: more `UnmarshalYAML` -> `UnmarshalText` * refactor: move `config.Upstream` into own file * refactor: add `Resolver.Type` method * squash: add `log` method to `typed` to use `Resolover.Type` as prefix * squash: tweak startup config logging * squash: add `LogResolverConfig` tests * squash: make sure all options of type `Duration` use `%s`
2023-03-12 22:14:10 +01:00
logger := log.WithPrefix(request.Log, queryLoggingResolverType)
2020-01-12 18:23:35 +01:00
start := time.Now()
resp, err := r.next.Resolve(ctx, request)
2020-01-12 18:23:35 +01:00
duration := time.Since(start).Milliseconds()
if err == nil {
select {
case r.logChan <- r.createLogEntry(request, resp, start, duration):
2020-01-12 18:23:35 +01:00
default:
logger.Error("query log writer is too slow, log entry will be dropped")
}
}
return resp, err
}
func (r *QueryLoggingResolver) createLogEntry(request *model.Request, response *model.Response,
start time.Time, durationMs int64,
) *querylog.LogEntry {
entry := querylog.LogEntry{
Start: start,
ClientIP: "0.0.0.0",
ClientNames: []string{"none"},
}
refactor: configuration rework (usage and printing) (#920) * refactor: make `config.Duration` a struct with `time.Duration` embed Allows directly calling `time.Duration` methods. * refactor(HostsFileResolver): don't copy individual config items The idea is to make adding configuration options easier, and searching for references straight forward. * refactor: move config printing to struct and use a logger Using a logger allows using multiple levels so the whole configuration can be printed in trace/verbose mode, but only important parts are shown by default. * squash: rename `Cast` to `ToDuration` * squash: revert `Duration` to a simple wrapper ("new type" pattern) * squash: `Duration.IsZero` tests * squash: refactor resolvers to rely on their config directly if possible * squash: implement `IsEnabled` and `LogValues` for all resolvers * refactor: use go-enum `--values` to simplify getting all log fields * refactor: simplify `QType` unmarshaling * squash: rename `ValueLogger` to `Configurable` * squash: rename `UpstreamConfig` to `ParallelBestConfig` * squash: rename `RewriteConfig` to `RewriterConfig` * squash: config tests * squash: resolver tests * squash: add `ForEach` test and improve `Chain` ones * squash: simplify implementing `config.Configurable` * squash: minor changes for better coverage * squash: more `UnmarshalYAML` -> `UnmarshalText` * refactor: move `config.Upstream` into own file * refactor: add `Resolver.Type` method * squash: add `log` method to `typed` to use `Resolover.Type` as prefix * squash: tweak startup config logging * squash: add `LogResolverConfig` tests * squash: make sure all options of type `Duration` use `%s`
2023-03-12 22:14:10 +01:00
for _, f := range r.cfg.Fields {
switch f {
case config.QueryLogFieldClientIP:
entry.ClientIP = request.ClientIP.String()
case config.QueryLogFieldClientName:
entry.ClientNames = request.ClientNames
case config.QueryLogFieldResponseReason:
entry.ResponseReason = response.Reason
entry.ResponseType = response.RType.String()
entry.ResponseCode = dns.RcodeToString[response.Res.Rcode]
case config.QueryLogFieldResponseAnswer:
entry.Answer = util.AnswerToString(response.Res.Answer)
case config.QueryLogFieldQuestion:
entry.QuestionName = util.Obfuscate(request.Req.Question[0].Name)
entry.QuestionType = dns.TypeToString[request.Req.Question[0].Qtype]
case config.QueryLogFieldDuration:
entry.DurationMs = durationMs
}
}
return &entry
}
2020-01-12 18:23:35 +01:00
// write entry: if log directory is configured, write to log file
func (r *QueryLoggingResolver) writeLog(ctx context.Context) {
for {
select {
case logEntry := <-r.logChan:
start := time.Now()
2020-01-12 18:23:35 +01:00
r.writer.Write(logEntry)
2020-01-12 18:23:35 +01:00
halfCap := cap(r.logChan) / 2 //nolint:gomnd
2021-01-19 21:52:24 +01:00
// if log channel is > 50% full, this could be a problem with slow writer (external storage over network etc.)
if len(r.logChan) > halfCap {
r.log().WithField("channel_len",
len(r.logChan)).Warnf("query log writer is too slow, write duration: %d ms", time.Since(start).Milliseconds())
}
case <-ctx.Done():
return
2020-01-12 18:23:35 +01:00
}
}
}