diff --git a/CHANGELOG.md b/CHANGELOG.md index 33c6ee66..d88206c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to ### Added +- New possible value of `6h` for `querylog_interval` setting ([#2504]). - Blocking access using client IDs ([#2624], [#3162]). - `source` directives support in `/etc/network/interfaces` on Linux ([#3257]). - RFC 9000 support in DNS-over-QUIC. @@ -40,6 +41,7 @@ and this project adheres to ### Changed +- `querylog_interval` setting is now formatted in hours. - Query log search now supports internationalized domains ([#3012]). - Internationalized domains are now shown decoded in the query log with the original encoded version shown in request details ([#3013]). @@ -82,6 +84,7 @@ released by then. [#2439]: https://github.com/AdguardTeam/AdGuardHome/issues/2439 [#2441]: https://github.com/AdguardTeam/AdGuardHome/issues/2441 [#2443]: https://github.com/AdguardTeam/AdGuardHome/issues/2443 +[#2504]: https://github.com/AdguardTeam/AdGuardHome/issues/2504 [#2624]: https://github.com/AdguardTeam/AdGuardHome/issues/2624 [#2763]: https://github.com/AdguardTeam/AdGuardHome/issues/2763 [#3012]: https://github.com/AdguardTeam/AdGuardHome/issues/3012 diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index c8accead..97f6564d 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -484,6 +484,7 @@ "encryption_key_source_content": "Paste the private key contents", "stats_params": "Statistics configuration", "config_successfully_saved": "Configuration successfully saved", + "interval_6_hour": "6 hours", "interval_24_hour": "24 hours", "interval_days": "{{count}} day", "interval_days_plural": "{{count}} days", diff --git a/client/src/components/Settings/LogsConfig/Form.js b/client/src/components/Settings/LogsConfig/Form.js index 17c5c45b..8db3f18d 100644 --- a/client/src/components/Settings/LogsConfig/Form.js +++ b/client/src/components/Settings/LogsConfig/Form.js @@ -4,26 +4,33 @@ import { Field, reduxForm } from 'redux-form'; import { Trans, withTranslation } from 'react-i18next'; import flow from 'lodash/flow'; -import { CheckboxField, renderRadioField, toNumber } from '../../../helpers/form'; +import { CheckboxField, renderRadioField, toFloatNumber } from '../../../helpers/form'; import { FORM_NAME, QUERY_LOG_INTERVALS_DAYS } from '../../../helpers/constants'; import '../FormButton.css'; -const getIntervalFields = (processing, t, toNumber) => QUERY_LOG_INTERVALS_DAYS.map((interval) => { - const title = interval === 1 ? t('interval_24_hour') : t('interval_days', { count: interval }); +const getIntervalTitle = (interval, t) => { + switch (interval) { + case 0.25: + return t('interval_6_hour'); + case 1: + return t('interval_24_hour'); + default: + return t('interval_days', { count: interval }); + } +}; - return ( - - ); -}); +const getIntervalFields = (processing, t, toNumber) => QUERY_LOG_INTERVALS_DAYS.map((interval) => ( + +)); const Form = (props) => { const { @@ -56,7 +63,7 @@ const Form = (props) => {
- {getIntervalFields(processing, t, toNumber)} + {getIntervalFields(processing, t, toFloatNumber)}
diff --git a/client/src/helpers/constants.js b/client/src/helpers/constants.js index 5b263370..0e9682f7 100644 --- a/client/src/helpers/constants.js +++ b/client/src/helpers/constants.js @@ -357,7 +357,7 @@ export const NOT_FILTERED = 'NotFiltered'; export const STATS_INTERVALS_DAYS = [0, 1, 7, 30, 90]; -export const QUERY_LOG_INTERVALS_DAYS = [1, 7, 30, 90]; +export const QUERY_LOG_INTERVALS_DAYS = [0.25, 1, 7, 30, 90]; export const FILTERS_INTERVALS_HOURS = [0, 1, 12, 24, 72, 168]; diff --git a/client/src/helpers/form.js b/client/src/helpers/form.js index e1e6aaa0..c4f90722 100644 --- a/client/src/helpers/form.js +++ b/client/src/helpers/form.js @@ -276,6 +276,12 @@ export const ip4ToInt = (ip) => { */ export const toNumber = (value) => value && parseInt(value, 10); +/** + * @param value {string} + * @returns {*|number} + */ +export const toFloatNumber = (value) => value && parseFloat(value, 10); + /** * @param value {string} * @returns {boolean} diff --git a/internal/home/config.go b/internal/home/config.go index 3af3ecf2..7b353ed9 100644 --- a/internal/home/config.go +++ b/internal/home/config.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "sync" + "time" "github.com/AdguardTeam/AdGuardHome/internal/dhcpd" "github.com/AdguardTeam/AdGuardHome/internal/dnsforward" @@ -102,11 +103,12 @@ type dnsConfig struct { // time interval for statistics (in days) StatsInterval uint32 `yaml:"statistics_interval"` - QueryLogEnabled bool `yaml:"querylog_enabled"` // if true, query log is enabled - QueryLogFileEnabled bool `yaml:"querylog_file_enabled"` // if true, query log will be written to a file - QueryLogInterval uint32 `yaml:"querylog_interval"` // time interval for query log (in days) - QueryLogMemSize uint32 `yaml:"querylog_size_memory"` // number of entries kept in memory before they are flushed to disk - AnonymizeClientIP bool `yaml:"anonymize_client_ip"` // anonymize clients' IP addresses in logs and stats + QueryLogEnabled bool `yaml:"querylog_enabled"` // if true, query log is enabled + QueryLogFileEnabled bool `yaml:"querylog_file_enabled"` // if true, query log will be written to a file + // QueryLogInterval is the interval for query log's files rotation. + QueryLogInterval Duration `yaml:"querylog_interval"` + QueryLogMemSize uint32 `yaml:"querylog_size_memory"` // number of entries kept in memory before they are flushed to disk + AnonymizeClientIP bool `yaml:"anonymize_client_ip"` // anonymize clients' IP addresses in logs and stats dnsforward.FilteringConfig `yaml:",inline"` @@ -185,7 +187,7 @@ var config = configuration{ }, FilteringEnabled: true, // whether or not use filter lists FiltersUpdateIntervalHours: 24, - UpstreamTimeout: Duration{dnsforward.DefaultTimeout}, + UpstreamTimeout: Duration{Duration: dnsforward.DefaultTimeout}, LocalDomainName: "lan", ResolveClients: true, UsePrivateRDNS: true, @@ -212,7 +214,7 @@ func initConfig() { config.DNS.QueryLogEnabled = true config.DNS.QueryLogFileEnabled = true - config.DNS.QueryLogInterval = 90 + config.DNS.QueryLogInterval = Duration{Duration: 90 * 24 * time.Hour} config.DNS.QueryLogMemSize = 1000 config.DNS.CacheSize = 4 * 1024 * 1024 @@ -281,7 +283,7 @@ func parseConfig() error { } if config.DNS.UpstreamTimeout.Duration == 0 { - config.DNS.UpstreamTimeout = Duration{dnsforward.DefaultTimeout} + config.DNS.UpstreamTimeout = Duration{Duration: dnsforward.DefaultTimeout} } return nil @@ -328,7 +330,7 @@ func (c *configuration) write() error { Context.queryLog.WriteDiskConfig(&dc) config.DNS.QueryLogEnabled = dc.Enabled config.DNS.QueryLogFileEnabled = dc.FileEnabled - config.DNS.QueryLogInterval = dc.RotationIvl + config.DNS.QueryLogInterval = Duration{Duration: dc.RotationIvl} config.DNS.QueryLogMemSize = dc.MemSize config.DNS.AnonymizeClientIP = dc.AnonymizeClientIP } diff --git a/internal/home/dns.go b/internal/home/dns.go index c531d1c2..8c0cc035 100644 --- a/internal/home/dns.go +++ b/internal/home/dns.go @@ -48,7 +48,7 @@ func initDNSServer() error { HTTPRegister: httpRegister, FindClient: Context.clients.findMultiple, BaseDir: baseDir, - RotationIvl: config.DNS.QueryLogInterval, + RotationIvl: config.DNS.QueryLogInterval.Duration, MemSize: config.DNS.QueryLogMemSize, Enabled: config.DNS.QueryLogEnabled, FileEnabled: config.DNS.QueryLogFileEnabled, diff --git a/internal/home/duration.go b/internal/home/duration.go index c5a2a751..2715384c 100644 --- a/internal/home/duration.go +++ b/internal/home/duration.go @@ -12,6 +12,35 @@ type Duration struct { time.Duration } +// String implements the fmt.Stringer interface for Duration. It wraps +// time.Duration.String method and additionally cuts off non-leading zero values +// of minutes and seconds. Some values which are differ between the +// implementations: +// +// Duration: "1m", time.Duration: "1m0s" +// Duration: "1h", time.Duration: "1h0m0s" +// Duration: "1h1m", time.Duration: "1h1m0s" +// +func (d Duration) String() (str string) { + str = d.Duration.String() + secs := d.Seconds() + var secsInt int + if secsInt = int(secs); float64(secsInt) != secs || secsInt%60 != 0 { + return str + } + + const ( + tailMin = len(`0s`) + tailMinSec = len(`0m0s`) + ) + + if (secsInt%3600)/60 != 0 { + return str[:len(str)-tailMin] + } + + return str[:len(str)-tailMinSec] +} + // MarshalText implements the encoding.TextMarshaler interface for Duration. func (d Duration) MarshalText() (text []byte, err error) { return []byte(d.String()), nil @@ -19,6 +48,8 @@ func (d Duration) MarshalText() (text []byte, err error) { // UnmarshalText implements the encoding.TextUnmarshaler interface for // *Duration. +// +// TODO(e.burkov): Make it able to parse larger units like days. func (d *Duration) UnmarshalText(b []byte) (err error) { defer func() { err = errors.Annotate(err, "unmarshalling duration: %w") }() diff --git a/internal/home/duration_test.go b/internal/home/duration_test.go index 8a9ad215..55d98ff8 100644 --- a/internal/home/duration_test.go +++ b/internal/home/duration_test.go @@ -12,6 +12,50 @@ import ( yaml "gopkg.in/yaml.v2" ) +func TestDuration_String(t *testing.T) { + testCases := []struct { + name string + val time.Duration + }{{ + name: "1s", + val: time.Second, + }, { + name: "1m", + val: time.Minute, + }, { + name: "1h", + val: time.Hour, + }, { + name: "1m1s", + val: time.Minute + time.Second, + }, { + name: "1h1m", + val: time.Hour + time.Minute, + }, { + name: "1h0m1s", + val: time.Hour + time.Second, + }, { + name: "1ms", + val: time.Millisecond, + }, { + name: "1h0m0.001s", + val: time.Hour + time.Millisecond, + }, { + name: "1.001s", + val: time.Second + time.Millisecond, + }, { + name: "1m1.001s", + val: time.Minute + time.Second + time.Millisecond, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + d := Duration{Duration: tc.val} + assert.Equal(t, tc.name, d.String()) + }) + } +} + // durationEncodingTester is a helper struct to simplify testing different // Duration marshalling and unmarshalling cases. type durationEncodingTester struct { diff --git a/internal/home/upgrade.go b/internal/home/upgrade.go index 1e471ce6..175e73f1 100644 --- a/internal/home/upgrade.go +++ b/internal/home/upgrade.go @@ -9,6 +9,7 @@ import ( "runtime" "strconv" "strings" + "time" "github.com/AdguardTeam/AdGuardHome/internal/aghnet" "github.com/AdguardTeam/golibs/errors" @@ -19,7 +20,7 @@ import ( ) // currentSchemaVersion is the current schema version. -const currentSchemaVersion = 11 +const currentSchemaVersion = 12 // These aliases are provided for convenience. type ( @@ -82,6 +83,7 @@ func upgradeConfigSchema(oldVersion int, diskConf yobj) (err error) { upgradeSchema8to9, upgradeSchema9to10, upgradeSchema10to11, + upgradeSchema11to12, } n := 0 @@ -647,6 +649,46 @@ func upgradeSchema10to11(diskConf yobj) (err error) { return nil } +// upgradeSchema11to12 performs the following changes: +// +// # BEFORE: +// 'querylog_interval': 90 +// +// # AFTER: +// 'querylog_interval': '2160h' +// +func upgradeSchema11to12(diskConf yobj) (err error) { + log.Printf("Upgrade yaml: 11 to 12") + diskConf["schema_version"] = 12 + + dnsVal, ok := diskConf["dns"] + if !ok { + return nil + } + + var dns yobj + dns, ok = dnsVal.(yobj) + if !ok { + return fmt.Errorf("unexpected type of dns: %T", dnsVal) + } + + const field = "querylog_interval" + + // Set the initial value from home.initConfig function. + qlogIvl := 90 + qlogIvlVal, ok := dns[field] + if ok { + qlogIvl, ok = qlogIvlVal.(int) + if !ok { + return fmt.Errorf("unexpected type of %s: %T", field, qlogIvlVal) + } + } + + dns[field] = Duration{Duration: time.Duration(qlogIvl) * 24 * time.Hour} + + return nil +} + // TODO(a.garipov): Replace with log.Output when we port it to our logging // package. func funcName() string { diff --git a/internal/home/upgrade_test.go b/internal/home/upgrade_test.go index fe6733e8..107bdf96 100644 --- a/internal/home/upgrade_test.go +++ b/internal/home/upgrade_test.go @@ -2,6 +2,7 @@ package home import ( "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -415,3 +416,98 @@ func TestUpgradeSchema10to11(t *testing.T) { check(t, conf) }) } + +func TestUpgradeSchema11to12(t *testing.T) { + testCases := []struct { + ivl any + want any + wantErr string + name string + }{{ + ivl: 1, + want: Duration{Duration: 24 * time.Hour}, + wantErr: "", + name: "success", + }, { + ivl: 0.25, + want: 0, + wantErr: "unexpected type of querylog_interval: float64", + name: "fail", + }} + + for _, tc := range testCases { + conf := yobj{ + "dns": yobj{ + "querylog_interval": tc.ivl, + }, + "schema_version": 11, + } + t.Run(tc.name, func(t *testing.T) { + err := upgradeSchema11to12(conf) + + if tc.wantErr != "" { + require.Error(t, err) + assert.Equal(t, tc.wantErr, err.Error()) + + return + } + + require.NoError(t, err) + require.Equal(t, conf["schema_version"], 12) + + dnsVal, ok := conf["dns"] + require.True(t, ok) + + var newDNSConf yobj + newDNSConf, ok = dnsVal.(yobj) + require.True(t, ok) + + var newIvl Duration + newIvl, ok = newDNSConf["querylog_interval"].(Duration) + require.True(t, ok) + + assert.Equal(t, tc.want, newIvl) + }) + } + + t.Run("no_dns", func(t *testing.T) { + err := upgradeSchema11to12(yobj{}) + + assert.NoError(t, err) + }) + + t.Run("bad_dns", func(t *testing.T) { + err := upgradeSchema11to12(yobj{ + "dns": 0, + }) + + require.Error(t, err) + assert.Equal(t, "unexpected type of dns: int", err.Error()) + }) + + t.Run("no_field", func(t *testing.T) { + conf := yobj{ + "dns": yobj{}, + } + + err := upgradeSchema11to12(conf) + require.NoError(t, err) + + dns, ok := conf["dns"] + require.True(t, ok) + + var dnsVal yobj + dnsVal, ok = dns.(yobj) + require.True(t, ok) + + var ivl interface{} + ivl, ok = dnsVal["querylog_interval"] + require.True(t, ok) + + var ivlVal Duration + ivlVal, ok = ivl.(Duration) + require.True(t, ok) + + assert.Equal(t, 90*24*time.Hour, ivlVal.Duration) + }) +} diff --git a/internal/querylog/http.go b/internal/querylog/http.go index f7f5edb4..f81552c2 100644 --- a/internal/querylog/http.go +++ b/internal/querylog/http.go @@ -16,9 +16,11 @@ import ( ) type qlogConfig struct { - Enabled bool `json:"enabled"` - Interval uint32 `json:"interval"` - AnonymizeClientIP bool `json:"anonymize_client_ip"` + Enabled bool `json:"enabled"` + // Use float64 here to support fractional numbers and not mess the API + // users by changing the units. + Interval float64 `json:"interval"` + AnonymizeClientIP bool `json:"anonymize_client_ip"` } // Register web handlers @@ -71,7 +73,7 @@ func (l *queryLog) handleQueryLogClear(_ http.ResponseWriter, _ *http.Request) { func (l *queryLog) handleQueryLogInfo(w http.ResponseWriter, r *http.Request) { resp := qlogConfig{} resp.Enabled = l.conf.Enabled - resp.Interval = l.conf.RotationIvl + resp.Interval = l.conf.RotationIvl.Hours() / 24 resp.AnonymizeClientIP = l.conf.AnonymizeClientIP jsonVal, err := json.Marshal(resp) @@ -95,7 +97,8 @@ func (l *queryLog) handleQueryLogConfig(w http.ResponseWriter, r *http.Request) return } - if req.Exists("interval") && !checkInterval(d.Interval) { + ivl := time.Duration(24*d.Interval) * time.Hour + if req.Exists("interval") && !checkInterval(ivl) { httpError(r, w, http.StatusBadRequest, "Unsupported interval") return } @@ -107,7 +110,7 @@ func (l *queryLog) handleQueryLogConfig(w http.ResponseWriter, r *http.Request) conf.Enabled = d.Enabled } if req.Exists("interval") { - conf.RotationIvl = d.Interval + conf.RotationIvl = ivl } if req.Exists("anonymize_client_ip") { conf.AnonymizeClientIP = d.AnonymizeClientIP diff --git a/internal/querylog/qlog.go b/internal/querylog/qlog.go index 5e670eac..f836aa8c 100644 --- a/internal/querylog/qlog.go +++ b/internal/querylog/qlog.go @@ -100,8 +100,17 @@ func (l *queryLog) Close() { _ = l.flushLogBuffer(true) } -func checkInterval(days uint32) bool { - return days == 1 || days == 7 || days == 30 || days == 90 +func checkInterval(ivl time.Duration) (ok bool) { + // The constants for possible values of query log's rotation interval. + const ( + quarterDay = 6 * time.Hour + day = 24 * time.Hour + week = day * 7 + month = day * 30 + threeMonths = day * 90 + ) + + return ivl == quarterDay || ivl == day || ivl == week || ivl == month || ivl == threeMonths } func (l *queryLog) WriteDiskConfig(c *Config) { diff --git a/internal/querylog/qlog_test.go b/internal/querylog/qlog_test.go index 9ee31aa9..9a641ae4 100644 --- a/internal/querylog/qlog_test.go +++ b/internal/querylog/qlog_test.go @@ -26,7 +26,7 @@ func TestQueryLog(t *testing.T) { l := newQueryLog(Config{ Enabled: true, FileEnabled: true, - RotationIvl: 1, + RotationIvl: 24 * time.Hour, MemSize: 100, BaseDir: t.TempDir(), }) @@ -128,7 +128,7 @@ func TestQueryLog(t *testing.T) { func TestQueryLogOffsetLimit(t *testing.T) { l := newQueryLog(Config{ Enabled: true, - RotationIvl: 1, + RotationIvl: 24 * time.Hour, MemSize: 100, BaseDir: t.TempDir(), }) @@ -153,33 +153,34 @@ func TestQueryLogOffsetLimit(t *testing.T) { testCases := []struct { name string + want string + wantLen int offset int limit int - wantLen int - want string }{{ name: "page_1", + want: firstPageDomain, + wantLen: 10, offset: 0, limit: 10, - wantLen: 10, - want: firstPageDomain, }, { name: "page_2", + want: secondPageDomain, + wantLen: 10, offset: 10, limit: 10, - wantLen: 10, - want: secondPageDomain, }, { name: "page_2.5", + want: secondPageDomain, + wantLen: 5, offset: 15, limit: 10, - wantLen: 5, - want: secondPageDomain, }, { name: "page_3", + want: "", + wantLen: 0, offset: 20, limit: 10, - wantLen: 0, }} for _, tc := range testCases { @@ -202,7 +203,7 @@ func TestQueryLogMaxFileScanEntries(t *testing.T) { l := newQueryLog(Config{ Enabled: true, FileEnabled: true, - RotationIvl: 1, + RotationIvl: 24 * time.Hour, MemSize: 100, BaseDir: t.TempDir(), }) @@ -230,7 +231,7 @@ func TestQueryLogFileDisabled(t *testing.T) { l := newQueryLog(Config{ Enabled: true, FileEnabled: false, - RotationIvl: 1, + RotationIvl: 24 * time.Hour, MemSize: 2, BaseDir: t.TempDir(), }) diff --git a/internal/querylog/querylog.go b/internal/querylog/querylog.go index bd158fc2..b0403fdf 100644 --- a/internal/querylog/querylog.go +++ b/internal/querylog/querylog.go @@ -41,10 +41,17 @@ type Config struct { // BaseDir is the base directory for log files. BaseDir string - // RotationIvl is the interval for log rotation, in days. After that - // period, the old log file will be renamed, NOT deleted, so the actual - // log retention time is twice the interval. - RotationIvl uint32 + // RotationIvl is the interval for log rotation. After that period, the + // old log file will be renamed, NOT deleted, so the actual log + // retention time is twice the interval. The value must be one of: + // + // 6 * time.Hour + // 24 * time.Hour + // 7 * 24 * time.Hour + // 30 * 24 * time.Hour + // 90 * 24 * time.Hour + // + RotationIvl time.Duration // MemSize is the number of entries kept in a memory buffer before they // are flushed to disk. @@ -118,7 +125,7 @@ func newQueryLog(conf Config) (l *queryLog) { "querylog: warning: unsupported rotation interval %d, setting to 1 day", conf.RotationIvl, ) - l.conf.RotationIvl = 1 + l.conf.RotationIvl = 24 * time.Hour } return l diff --git a/internal/querylog/querylogfile.go b/internal/querylog/querylogfile.go index aac9a5f6..4acaab1f 100644 --- a/internal/querylog/querylogfile.go +++ b/internal/querylog/querylogfile.go @@ -104,43 +104,52 @@ func (l *queryLog) rotate() error { return nil } -func (l *queryLog) readFileFirstTimeValue() int64 { - f, err := os.Open(l.logFile) +func (l *queryLog) readFileFirstTimeValue() (first time.Time, err error) { + var f *os.File + f, err = os.Open(l.logFile) if err != nil { - return -1 + return time.Time{}, err } - defer func() { - derr := f.Close() - if derr != nil { - log.Error("querylog: closing file: %s", derr) - } - }() - buf := make([]byte, 500) - r, err := f.Read(buf) + defer func() { err = errors.WithDeferred(err, f.Close()) }() + + buf := make([]byte, 512) + var r int + r, err = f.Read(buf) if err != nil { - return -1 + return time.Time{}, err } - buf = buf[:r] - val := readJSONValue(string(buf), `"T":"`) + val := readJSONValue(string(buf[:r]), `"T":"`) t, err := time.Parse(time.RFC3339Nano, val) if err != nil { - return -1 + return time.Time{}, err } log.Debug("querylog: the oldest log entry: %s", val) - return t.Unix() + + return t, nil } func (l *queryLog) periodicRotate() { - intervalSeconds := uint64(l.conf.RotationIvl) * 24 * 60 * 60 + defer log.OnPanic("querylog: rotating") + + var err error for { - oldest := l.readFileFirstTimeValue() - if uint64(oldest)+intervalSeconds <= uint64(time.Now().Unix()) { - _ = l.rotate() + var oldest time.Time + oldest, err = l.readFileFirstTimeValue() + if err != nil { + log.Debug("%s", err) } + if oldest.Add(l.conf.RotationIvl).After(time.Now()) { + err = l.rotate() + if err != nil { + log.Debug("%s", err) + } + } + + // What? time.Sleep(24 * time.Hour) } } diff --git a/internal/querylog/search_test.go b/internal/querylog/search_test.go index 5a1fe871..e2934cea 100644 --- a/internal/querylog/search_test.go +++ b/internal/querylog/search_test.go @@ -37,7 +37,7 @@ func TestQueryLog_Search_findClient(t *testing.T) { l := newQueryLog(Config{ FindClient: findClient, BaseDir: t.TempDir(), - RotationIvl: 1, + RotationIvl: 24 * time.Hour, MemSize: 100, Enabled: true, FileEnabled: true, diff --git a/openapi/CHANGELOG.md b/openapi/CHANGELOG.md index faf4c6b3..3e2f0577 100644 --- a/openapi/CHANGELOG.md +++ b/openapi/CHANGELOG.md @@ -4,7 +4,17 @@ ## v0.107: API changes -### Client IDs in Access Settings +### New possible value of `"interval"` field in `QueryLogConfig` + +* The value of `"interval"` field in `POST /control/querylog_config` and `GET + /control/querylog_info` methods could now take the value of `0.25`. It's + equal to 6 hours. + +* All the possible values of `"interval"` field are enumerated. + +* The type of `"interval"` field is now `number` instead of `integer`. + +### Client IDs in Access Settings * The `POST /control/access/set` HTTP API now accepts client IDs in `"allowed_clients"` and `"disallowed_clients"` fields. diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 06db336e..f2fdca6b 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -2008,8 +2008,15 @@ 'type': 'boolean' 'description': 'Is query log enabled' 'interval': - 'type': 'integer' - 'description': 'Time period to keep data (1 | 7 | 30 | 90)' + 'description': > + Time period for query log rotation. + 'type': 'number' + 'enum': + - 0.25 + - 1 + - 7 + - 30 + - 90 'anonymize_client_ip': 'type': 'boolean' 'description': "Anonymize clients' IP addresses"