all: imp code; fix notes

This commit is contained in:
Stanislav Chzhen 2023-02-27 13:20:53 +03:00
parent 3e5cf17623
commit e1d0c52340
10 changed files with 106 additions and 146 deletions

View File

@ -62,14 +62,18 @@ In this release, the schema version has changed from 16 to 17.
- The `GET /control/stats_info` HTTP API; use the new `GET - The `GET /control/stats_info` HTTP API; use the new `GET
/control/stats/config` API instead. /control/stats/config` API instead.
**NOTE:** If interval is custom then it will be equal to `90` days for **NOTE:** If `interval` was configured by editing configuration file or new
HTTP API call `PUT /control/stats/config/update` and it's not equal to
previous allowed enum values then it will be equal to `90` days for
compatibility reasons. compatibility reasons.
- The `POST /control/stats_config` HTTP API; use the new `PUT - The `POST /control/stats_config` HTTP API; use the new `PUT
/control/stats/config/update` API instead. /control/stats/config/update` API instead.
- The `GET /control/querylog_info` HTTP API; use the new `GET - The `GET /control/querylog_info` HTTP API; use the new `GET
/control/querylog/config` API instead. /control/querylog/config` API instead.
**NOTE:** If interval is custom then it will be equal to `90` days for **NOTE:** If `interval` was configured by editing configuration file or new
HTTP API call `PUT /control/querylog/config/update` and it's not equal to
previous allowed enum values then it will be equal to `90` days for
compatibility reasons. compatibility reasons.
- The `POST /control/querylog_config` HTTP API; use the new `PUT - The `POST /control/querylog_config` HTTP API; use the new `PUT
/control/querylog/config/update` API instead. /control/querylog/config/update` API instead.

View File

@ -71,16 +71,16 @@ func GenerateHostname(ip net.IP) (hostname string) {
func NewDomainNameSet(list []string) (set *stringutil.Set, err error) { func NewDomainNameSet(list []string) (set *stringutil.Set, err error) {
set = stringutil.NewSet() set = stringutil.NewSet()
for _, v := range list { for i, v := range list {
host := strings.ToLower(strings.TrimSuffix(v, ".")) host := strings.ToLower(strings.TrimSuffix(v, "."))
// TODO(a.garipov): Think about ignoring empty (".") names in // TODO(a.garipov): Think about ignoring empty (".") names in the
// the future. // future.
if host == "" { if host == "" {
return nil, errors.Error("host name is empty") return nil, errors.Error("host name is empty")
} }
if set.Has(host) { if set.Has(host) {
return nil, fmt.Errorf("duplicate host name %q", host) return nil, fmt.Errorf("duplicate host name %q at index %d", host, i)
} }
set.Add(host) set.Add(host)

View File

@ -218,33 +218,32 @@ type tlsConfigSettings struct {
} }
type queryLogConfig struct { type queryLogConfig struct {
// Ignored is the list of host names, which should not be written to log.
Ignored []string `yaml:"ignored"`
// Interval is the interval for query log's files rotation.
Interval timeutil.Duration `yaml:"interval"`
// MemSize is the number of entries kept in memory before they are flushed
// to disk.
MemSize uint32 `yaml:"size_memory"`
// Enabled defines if the query log is enabled. // Enabled defines if the query log is enabled.
Enabled bool `yaml:"enabled"` Enabled bool `yaml:"enabled"`
// FileEnabled defines, if the query log is written to the file. // FileEnabled defines, if the query log is written to the file.
FileEnabled bool `yaml:"file_enabled"` FileEnabled bool `yaml:"file_enabled"`
// Interval is the interval for query log's files rotation.
Interval timeutil.Duration `yaml:"interval"`
// MemSize is the number of entries kept in memory before they are
// flushed to disk.
MemSize uint32 `yaml:"size_memory"`
// Ignored is the list of host names, which should not be written to
// log.
Ignored []string `yaml:"ignored"`
} }
type statsConfig struct { type statsConfig struct {
// Enabled defines if the statistics are enabled.
Enabled bool `yaml:"enabled"`
// Interval is the time interval for flushing statistics to the disk.
Interval timeutil.Duration `yaml:"interval"`
// Ignored is the list of host names, which should not be counted. // Ignored is the list of host names, which should not be counted.
Ignored []string `yaml:"ignored"` Ignored []string `yaml:"ignored"`
// Interval is the retention interval for statistics.
Interval timeutil.Duration `yaml:"interval"`
// Enabled defines if the statistics are enabled.
Enabled bool `yaml:"enabled"`
} }
// config is the global configuration structure. // config is the global configuration structure.

View File

@ -7,6 +7,7 @@ import (
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"sort"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -26,8 +27,8 @@ type configJSON struct {
// fractional numbers and not mess the API users by changing the units. // fractional numbers and not mess the API users by changing the units.
Interval float64 `json:"interval"` Interval float64 `json:"interval"`
// Enabled shows if the querylog is enabled. It is an [aghalg.NullBool] // Enabled shows if the querylog is enabled. It is an aghalg.NullBool to
// to be able to tell when it's set without using pointers. // be able to tell when it's set without using pointers.
Enabled aghalg.NullBool `json:"enabled"` Enabled aghalg.NullBool `json:"enabled"`
// AnonymizeClientIP shows if the clients' IP addresses must be anonymized. // AnonymizeClientIP shows if the clients' IP addresses must be anonymized.
@ -38,24 +39,22 @@ type configJSON struct {
// getConfigResp is the JSON structure for the querylog configuration. // getConfigResp is the JSON structure for the querylog configuration.
type getConfigResp struct { type getConfigResp struct {
// Enabled shows if the querylog is enabled. It is an // Ignored is the list of host names, which should not be written to log.
// [aghalg.NullBool] to be able to tell when it's set without using Ignored []string `json:"ignored"`
// pointers.
Enabled aghalg.NullBool `json:"enabled"`
// AnonymizeClientIP shows if the clients' IP addresses must be
// anonymized. It is an [aghalg.NullBool] to be able to tell when it's
// set without using pointers.
//
// TODO(a.garipov): Consider using separate setting for statistics.
AnonymizeClientIP aghalg.NullBool `json:"anonymize_client_ip"`
// Interval is the querylog rotation interval in milliseconds // Interval is the querylog rotation interval in milliseconds
Interval float64 `json:"interval"` Interval float64 `json:"interval"`
// Ignored is the list of host names, which should not be written to // Enabled shows if the querylog is enabled. It is an aghalg.NullBool to
// log. // be able to tell when it's set without using pointers.
Ignored []string `json:"ignored"` Enabled aghalg.NullBool `json:"enabled"`
// AnonymizeClientIP shows if the clients' IP addresses must be anonymized.
// It is an aghalg.NullBool to be able to tell when it's set without using
// pointers.
//
// TODO(a.garipov): Consider using separate setting for statistics.
AnonymizeClientIP aghalg.NullBool `json:"anonymize_client_ip"`
} }
// Register web handlers // Register web handlers
@ -109,8 +108,8 @@ func (l *queryLog) handleQueryLogInfo(w http.ResponseWriter, r *http.Request) {
ok := checkInterval(ivl) ok := checkInterval(ivl)
if !ok { if !ok {
// NOTE: If interval is custom we set it to 90 days for // NOTE: If interval is custom we set it to 90 days for compatibility
// compatibility with old API. // with old API.
ivl = timeutil.Day * 90 ivl = timeutil.Day * 90
} }
@ -128,6 +127,7 @@ func (l *queryLog) handleGetQueryLogConfig(w http.ResponseWriter, r *http.Reques
defer l.lock.Unlock() defer l.lock.Unlock()
ignored := l.conf.Ignored.Values() ignored := l.conf.Ignored.Values()
sort.Strings(ignored)
_ = aghhttp.WriteJSONResponse(w, r, getConfigResp{ _ = aghhttp.WriteJSONResponse(w, r, getConfigResp{
Enabled: aghalg.BoolToNullBool(l.conf.Enabled), Enabled: aghalg.BoolToNullBool(l.conf.Enabled),
Interval: float64(l.conf.RotationIvl.Milliseconds()), Interval: float64(l.conf.RotationIvl.Milliseconds()),
@ -257,7 +257,7 @@ func (l *queryLog) handlePutQueryLogConfig(w http.ResponseWriter, r *http.Reques
if len(newConf.Ignored) > 0 { if len(newConf.Ignored) > 0 {
set, serr := aghnet.NewDomainNameSet(newConf.Ignored) set, serr := aghnet.NewDomainNameSet(newConf.Ignored)
if serr != nil { if serr != nil {
aghhttp.Error(r, w, http.StatusUnprocessableEntity, "ignored: %s", serr) aghhttp.Error(r, w, http.StatusUnprocessableEntity, "ignored: duplicate or empty host")
return return
} }

View File

@ -157,11 +157,8 @@ func newQueryLog(conf Config) (l *queryLog, err error) {
l.conf = &Config{} l.conf = &Config{}
*l.conf = conf *l.conf = conf
if conf.RotationIvl < time.Hour { if conf.RotationIvl < time.Hour || conf.RotationIvl > timeutil.Day*365 {
return nil, errors.Error("interval: less than an hour") return nil, errors.Error("unsupported interval")
}
if conf.RotationIvl > timeutil.Day*365 {
return nil, errors.Error("interval: more than a year")
} }
return l, nil return l, nil

View File

@ -5,6 +5,7 @@ package stats
import ( import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"sort"
"time" "time"
"github.com/AdguardTeam/AdGuardHome/internal/aghalg" "github.com/AdguardTeam/AdGuardHome/internal/aghalg"
@ -68,15 +69,15 @@ type configResp struct {
// getConfigResp is the response to the GET /control/stats_info. // getConfigResp is the response to the GET /control/stats_info.
type getConfigResp struct { type getConfigResp struct {
// Enabled shows if statistics are enabled. It is an [aghalg.NullBool] // Ignored is the list of host names, which should not be counted.
// to be able to tell when it's set without using pointers. Ignored []string `json:"ignored"`
Enabled aghalg.NullBool `json:"enabled"`
// Interval is the statistics rotation interval in milliseconds. // Interval is the statistics rotation interval in milliseconds.
Interval float64 `json:"interval"` Interval float64 `json:"interval"`
// Ignored is the list of host names, which should not be counted. // Enabled shows if statistics are enabled. It is an aghalg.NullBool to be
Ignored []string `json:"ignored"` // able to tell when it's set without using pointers.
Enabled aghalg.NullBool `json:"enabled"`
} }
// handleStatsInfo handles requests to the GET /control/stats_info endpoint. // handleStatsInfo handles requests to the GET /control/stats_info endpoint.
@ -90,8 +91,8 @@ func (s *StatsCtx) handleStatsInfo(w http.ResponseWriter, r *http.Request) {
ok := checkInterval(days) ok := checkInterval(days)
if !ok || (s.enabled && days == 0) { if !ok || (s.enabled && days == 0) {
// NOTE: If interval is custom we set it to 90 days for // NOTE: If interval is custom we set it to 90 days for compatibility
// compatibility with old API. // with old API.
days = 90 days = 90
} }
@ -102,15 +103,19 @@ func (s *StatsCtx) handleStatsInfo(w http.ResponseWriter, r *http.Request) {
_ = aghhttp.WriteJSONResponse(w, r, resp) _ = aghhttp.WriteJSONResponse(w, r, resp)
} }
// handleGetStatsConfig handles requests to the GET /control/stats_info endpoint. // handleGetStatsConfig handles requests to the GET /control/stats_info
// endpoint.
func (s *StatsCtx) handleGetStatsConfig(w http.ResponseWriter, r *http.Request) { func (s *StatsCtx) handleGetStatsConfig(w http.ResponseWriter, r *http.Request) {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
ignored := s.ignored.Values()
sort.Strings(ignored)
resp := getConfigResp{ resp := getConfigResp{
Enabled: aghalg.BoolToNullBool(s.enabled), Enabled: aghalg.BoolToNullBool(s.enabled),
Interval: float64(s.limit.Milliseconds()), Interval: float64(s.limit.Milliseconds()),
Ignored: s.ignored.Values(), Ignored: ignored,
} }
err := aghhttp.WriteJSONResponse(w, r, resp) err := aghhttp.WriteJSONResponse(w, r, resp)
if err != nil { if err != nil {
@ -182,7 +187,7 @@ func (s *StatsCtx) handlePutStatsConfig(w http.ResponseWriter, r *http.Request)
if len(reqData.Ignored) > 0 { if len(reqData.Ignored) > 0 {
set, serr := aghnet.NewDomainNameSet(reqData.Ignored) set, serr := aghnet.NewDomainNameSet(reqData.Ignored)
if serr != nil { if serr != nil {
aghhttp.Error(r, w, http.StatusUnprocessableEntity, "ignored: %s", serr) aghhttp.Error(r, w, http.StatusUnprocessableEntity, "ignored: duplicate or empty host")
return return
} }

View File

@ -17,22 +17,30 @@ import (
) )
func TestHandleStatsConfig(t *testing.T) { func TestHandleStatsConfig(t *testing.T) {
var ( const (
halfHour = time.Hour / 2 smallIvl = 1 * time.Minute
hour = time.Hour minIvl = 1 * time.Hour
year = timeutil.Day * 365 maxIvl = 365 * timeutil.Day
) )
conf := Config{
Filename: filepath.Join(t.TempDir(), "stats.db"),
Limit: time.Hour * 24,
Enabled: true,
UnitID: func() (id uint32) { return 0 },
ConfigModified: func() {},
}
testCases := []struct { testCases := []struct {
name string name string
body getConfigResp body getConfigResp
wantCode int wantCode int
wantErr string wantErr string
}{{ }{{
name: "set_ivl_1_hour", name: "set_ivl_1_minIvl",
body: getConfigResp{ body: getConfigResp{
Enabled: aghalg.NBTrue, Enabled: aghalg.NBTrue,
Interval: float64(hour.Milliseconds()), Interval: float64(minIvl.Milliseconds()),
Ignored: nil, Ignored: nil,
}, },
wantCode: http.StatusOK, wantCode: http.StatusOK,
@ -41,7 +49,7 @@ func TestHandleStatsConfig(t *testing.T) {
name: "small_interval", name: "small_interval",
body: getConfigResp{ body: getConfigResp{
Enabled: aghalg.NBTrue, Enabled: aghalg.NBTrue,
Interval: float64(halfHour.Milliseconds()), Interval: float64(smallIvl.Milliseconds()),
Ignored: []string{}, Ignored: []string{},
}, },
wantCode: http.StatusUnprocessableEntity, wantCode: http.StatusUnprocessableEntity,
@ -50,16 +58,16 @@ func TestHandleStatsConfig(t *testing.T) {
name: "big_interval", name: "big_interval",
body: getConfigResp{ body: getConfigResp{
Enabled: aghalg.NBTrue, Enabled: aghalg.NBTrue,
Interval: float64(year.Milliseconds() + hour.Milliseconds()), Interval: float64(maxIvl.Milliseconds() + minIvl.Milliseconds()),
Ignored: []string{}, Ignored: []string{},
}, },
wantCode: http.StatusUnprocessableEntity, wantCode: http.StatusUnprocessableEntity,
wantErr: "unsupported interval\n", wantErr: "unsupported interval\n",
}, { }, {
name: "set_ignored_ivl_1_year", name: "set_ignored_ivl_1_maxIvl",
body: getConfigResp{ body: getConfigResp{
Enabled: aghalg.NBTrue, Enabled: aghalg.NBTrue,
Interval: float64(year.Milliseconds()), Interval: float64(maxIvl.Milliseconds()),
Ignored: []string{ Ignored: []string{
"ignor.ed", "ignor.ed",
}, },
@ -70,30 +78,30 @@ func TestHandleStatsConfig(t *testing.T) {
name: "ignored_duplicate", name: "ignored_duplicate",
body: getConfigResp{ body: getConfigResp{
Enabled: aghalg.NBTrue, Enabled: aghalg.NBTrue,
Interval: float64(hour.Milliseconds()), Interval: float64(minIvl.Milliseconds()),
Ignored: []string{ Ignored: []string{
"ignor.ed", "ignor.ed",
"ignor.ed", "ignor.ed",
}, },
}, },
wantCode: http.StatusUnprocessableEntity, wantCode: http.StatusUnprocessableEntity,
wantErr: "ignored: duplicate host name \"ignor.ed\"\n", wantErr: "ignored: duplicate or empty host\n",
}, { }, {
name: "ignored_empty", name: "ignored_empty",
body: getConfigResp{ body: getConfigResp{
Enabled: aghalg.NBTrue, Enabled: aghalg.NBTrue,
Interval: float64(hour.Milliseconds()), Interval: float64(minIvl.Milliseconds()),
Ignored: []string{ Ignored: []string{
"", "",
}, },
}, },
wantCode: http.StatusUnprocessableEntity, wantCode: http.StatusUnprocessableEntity,
wantErr: "ignored: host name is empty\n", wantErr: "ignored: duplicate or empty host\n",
}, { }, {
name: "enabled_is_null", name: "enabled_is_null",
body: getConfigResp{ body: getConfigResp{
Enabled: aghalg.NBNull, Enabled: aghalg.NBNull,
Interval: float64(hour.Milliseconds()), Interval: float64(minIvl.Milliseconds()),
Ignored: []string{}, Ignored: []string{},
}, },
wantCode: http.StatusUnprocessableEntity, wantCode: http.StatusUnprocessableEntity,
@ -102,23 +110,6 @@ func TestHandleStatsConfig(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
handlers := map[string]http.Handler{}
conf := Config{
Filename: filepath.Join(t.TempDir(), "stats.db"),
Limit: time.Hour * 24,
Enabled: true,
UnitID: func() (id uint32) { return 0 },
HTTPRegister: func(
_ string,
url string,
handler http.HandlerFunc,
) {
handlers[url] = handler
},
ConfigModified: func() {},
}
s, err := New(conf) s, err := New(conf)
require.NoError(t, err) require.NoError(t, err)
@ -136,11 +127,10 @@ func TestHandleStatsConfig(t *testing.T) {
req := httptest.NewRequest(http.MethodPut, configPut, bytes.NewReader(buf)) req := httptest.NewRequest(http.MethodPut, configPut, bytes.NewReader(buf))
rw := httptest.NewRecorder() rw := httptest.NewRecorder()
handlers[configPut].ServeHTTP(rw, req) s.handlePutStatsConfig(rw, req)
require.Equal(t, tc.wantCode, rw.Code) require.Equal(t, tc.wantCode, rw.Code)
if tc.wantCode != http.StatusOK { if tc.wantCode != http.StatusOK {
assert.Equal(t, tc.wantCode, rw.Code)
assert.Equal(t, tc.wantErr, rw.Body.String()) assert.Equal(t, tc.wantErr, rw.Body.String())
return return
@ -149,12 +139,12 @@ func TestHandleStatsConfig(t *testing.T) {
resp := httptest.NewRequest(http.MethodGet, configGet, nil) resp := httptest.NewRequest(http.MethodGet, configGet, nil)
rw = httptest.NewRecorder() rw = httptest.NewRecorder()
handlers[configGet].ServeHTTP(rw, resp) s.handleGetStatsConfig(rw, resp)
require.Equal(t, http.StatusOK, rw.Code) require.Equal(t, http.StatusOK, rw.Code)
ans := getConfigResp{} ans := getConfigResp{}
jerr := json.Unmarshal(rw.Body.Bytes(), &ans) err = json.Unmarshal(rw.Body.Bytes(), &ans)
require.NoError(t, jerr) require.NoError(t, err)
assert.Equal(t, tc.body, ans) assert.Equal(t, tc.body, ans)
}) })

View File

@ -128,12 +128,8 @@ func New(conf Config) (s *StatsCtx, err error) {
ignored: conf.Ignored, ignored: conf.Ignored,
} }
if conf.Limit < time.Hour { if conf.Limit < time.Hour || conf.Limit > timeutil.Day*365 {
return nil, errors.Error("interval: less than an hour") return nil, errors.Error("unsupported interval")
}
if conf.Limit > timeutil.Day*365 {
return nil, errors.Error("interval: more than a year")
} }
s.limit = conf.Limit s.limit = conf.Limit

View File

@ -6,18 +6,20 @@
## v0.107.25: API changes ## v0.107.26: API changes
### Deprecated statistics APIs ### Deprecated statistics APIs
* The `GET /control/stats_info` HTTP API; use the new `GET * The `GET /control/stats_info` HTTP API; use the new `GET
/control/stats/config` API instead. /control/stats/config` API instead.
* The `POST /control/stats_config` HTTP API; use the new `PUT * The `POST /control/stats_config` HTTP API; use the new `PUT
/control/stats/config/update` API instead. /control/stats/config/update` API instead.
### New statistics APIs ### New statistics APIs
* The new `GET /control/stats/config` HTTP API. * The new `GET /control/stats/config` HTTP API.
* The new `PUT /control/stats/config/update` HTTP API allows config updates. * The new `PUT /control/stats/config/update` HTTP API allows config updates.
These `control/stats/config/update` and `control/stats/config` APIs accept and These `control/stats/config/update` and `control/stats/config` APIs accept and
@ -35,12 +37,14 @@ return a JSON object with the following format:
* The `GET /control/querylog_info` HTTP API; use the new `GET * The `GET /control/querylog_info` HTTP API; use the new `GET
/control/querylog/config` API instead. /control/querylog/config` API instead.
* The `POST /control/querylog_config` HTTP API; use the new `PUT * The `POST /control/querylog_config` HTTP API; use the new `PUT
/control/querylog/config/update` API instead. /control/querylog/config/update` API instead.
### New query log APIs ### New query log APIs
* The new `GET /control/querylog/config` HTTP API. * The new `GET /control/querylog/config` HTTP API.
* The new `PUT /control/querylog/config/update` HTTP API allows config updates. * The new `PUT /control/querylog/config/update` HTTP API allows config updates.
These `control/querylog/config/update` and `control/querylog/config` APIs These `control/querylog/config/update` and `control/querylog/config` APIs

View File

@ -227,10 +227,12 @@
'/querylog_info': '/querylog_info':
'get': 'get':
'deprecated': true 'deprecated': true
'description': > 'description': |
Deprecated: Use `GET /querylog/config` instead. Deprecated: Use `GET /querylog/config` instead.
NOTE: If interval is custom then it will be equal to `90` days for NOTE: If `interval` was configured by editing configuration file or new
HTTP API call `PUT /querylog/config/update` and it's not equal to
previous allowed enum values then it will be equal to `90` days for
compatibility reasons. compatibility reasons.
'tags': 'tags':
- 'log' - 'log'
@ -322,10 +324,12 @@
'/stats_info': '/stats_info':
'get': 'get':
'deprecated': true 'deprecated': true
'description': > 'description': |
Deprecated: Use `GET /stats/config` instead. Deprecated: Use `GET /stats/config` instead.
NOTE: If interval is custom then it will be equal to `90` days for NOTE: If `interval` was configured by editing configuration file or new
HTTP API call `PUT /stats/config/update` and it's not equal to
previous allowed enum values then it will be equal to `90` days for
compatibility reasons. compatibility reasons.
'tags': 'tags':
- 'stats' - 'stats'
@ -1748,24 +1752,7 @@
'items': 'items':
'type': 'string' 'type': 'string'
'PutStatsConfigUpdateRequest': 'PutStatsConfigUpdateRequest':
'type': 'object' '$ref': '#/components/schemas/GetStatsConfigResponse'
'description': 'Statistics configuration'
'required':
- 'enabled'
- 'interval'
- 'ignored'
'properties':
'enabled':
'description': 'Are statistics enabled'
'type': 'boolean'
'interval':
'description': 'Statistics rotation interval'
'type': 'number'
'ignored':
'description': 'List of host names, which should not be counted'
'type': 'array'
'items':
'type': 'string'
'DhcpConfig': 'DhcpConfig':
'type': 'object' 'type': 'object'
'properties': 'properties':
@ -2194,29 +2181,7 @@
'items': 'items':
'type': 'string' 'type': 'string'
'PutQueryLogConfigUpdateRequest': 'PutQueryLogConfigUpdateRequest':
'type': 'object' '$ref': '#/components/schemas/GetQueryLogConfigResponse'
'description': 'Query log configuration'
'required':
- 'enabled'
- 'interval'
- 'anonymize_client_ip'
- 'ignored'
'properties':
'enabled':
'type': 'boolean'
'description': 'Is query log enabled'
'interval':
'description': >
Time period for query log rotation.
'type': 'number'
'anonymize_client_ip':
'type': 'boolean'
'description': "Anonymize clients' IP addresses"
'ignored':
'description': 'List of host names, which should not be written to log'
'type': 'array'
'items':
'type': 'string'
'ResultRule': 'ResultRule':
'description': 'Applied rule.' 'description': 'Applied rule.'
'properties': 'properties':