diff --git a/CHANGELOG.md b/CHANGELOG.md
index 66f3b1f7ffb..b2fb8a02a9e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,9 +15,12 @@ and this project adheres to
### Added
+- Completely disabling statistics by setting the statistics interval to zero
+ ([#2141]).
- The ability to completely purge DHCP leases ([#1691]).
-- The ability to set the timeout for querying the upstream servers ([#2280]).
-- The ability to change group and user ID on startup on Unix ([#2763]).
+- Settable timeouts for querying the upstream servers ([#2280]).
+- Configuration file parameters to change group and user ID on startup on Unix
+ ([#2763]).
- Experimental OpenBSD support for AMD64 and 64-bit ARM CPUs ([#2439]).
- Support for custom port in DNS-over-HTTPS profiles for Apple's devices
([#3172]).
@@ -67,6 +70,7 @@ released by then.
[#1381]: https://github.com/AdguardTeam/AdGuardHome/issues/1381
[#1691]: https://github.com/AdguardTeam/AdGuardHome/issues/1691
+[#2141]: https://github.com/AdguardTeam/AdGuardHome/issues/2141
[#2280]: https://github.com/AdguardTeam/AdGuardHome/issues/2280
[#2439]: https://github.com/AdguardTeam/AdGuardHome/issues/2439
[#2441]: https://github.com/AdguardTeam/AdGuardHome/issues/2441
diff --git a/client/src/components/Settings/StatsConfig/Form.js b/client/src/components/Settings/StatsConfig/Form.js
index c16b39b24b6..60a5ae860d6 100644
--- a/client/src/components/Settings/StatsConfig/Form.js
+++ b/client/src/components/Settings/StatsConfig/Form.js
@@ -8,22 +8,27 @@ import { renderRadioField, toNumber } from '../../../helpers/form';
import { FORM_NAME, STATS_INTERVALS_DAYS } from '../../../helpers/constants';
import '../FormButton.css';
-const getIntervalFields = (processing, t, toNumber) => STATS_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:
+ return t('disabled');
+ case 1:
+ return t('interval_24_hour');
+ default:
+ return t('interval_days', { count: interval });
+ }
+};
- return (
-
- );
-});
+const getIntervalFields = (processing, t, toNumber) => STATS_INTERVALS_DAYS.map((interval) => );
const Form = (props) => {
const {
diff --git a/client/src/helpers/constants.js b/client/src/helpers/constants.js
index 4a81d5801e3..5b2633705d8 100644
--- a/client/src/helpers/constants.js
+++ b/client/src/helpers/constants.js
@@ -355,7 +355,7 @@ export const ENCRYPTION_SOURCE = {
export const FILTERED = 'Filtered';
export const NOT_FILTERED = 'NotFiltered';
-export const STATS_INTERVALS_DAYS = [1, 7, 30, 90];
+export const STATS_INTERVALS_DAYS = [0, 1, 7, 30, 90];
export const QUERY_LOG_INTERVALS_DAYS = [1, 7, 30, 90];
diff --git a/internal/stats/http.go b/internal/stats/http.go
index 1580174a388..e828a2b57c2 100644
--- a/internal/stats/http.go
+++ b/internal/stats/http.go
@@ -19,6 +19,10 @@ func httpError(r *http.Request, w http.ResponseWriter, code int, format string,
http.Error(w, text, code)
}
+// topAddrs is an alias for the types of the TopFoo fields of statsResponse.
+// The key is either a client's address or a requested address.
+type topAddrs = map[string]uint64
+
// statsResponse is a response for getting statistics.
type statsResponse struct {
TimeUnits string `json:"time_units"`
@@ -31,9 +35,9 @@ type statsResponse struct {
AvgProcessingTime float64 `json:"avg_processing_time"`
- TopQueried []map[string]uint64 `json:"top_queried_domains"`
- TopClients []map[string]uint64 `json:"top_clients"`
- TopBlocked []map[string]uint64 `json:"top_blocked_domains"`
+ TopQueried []topAddrs `json:"top_queried_domains"`
+ TopClients []topAddrs `json:"top_clients"`
+ TopBlocked []topAddrs `json:"top_blocked_domains"`
DNSQueries []uint64 `json:"dns_queries"`
@@ -45,17 +49,37 @@ type statsResponse struct {
// handleStats is a handler for getting statistics.
func (s *statsCtx) handleStats(w http.ResponseWriter, r *http.Request) {
start := time.Now()
- response, ok := s.getData()
- log.Debug("Stats: prepared data in %v", time.Since(start))
- if !ok {
- httpError(r, w, http.StatusInternalServerError, "Couldn't get statistics data")
+ var resp statsResponse
+ if s.conf.limit == 0 {
+ resp = statsResponse{
+ TimeUnits: "days",
- return
+ TopBlocked: []topAddrs{},
+ TopClients: []topAddrs{},
+ TopQueried: []topAddrs{},
+
+ BlockedFiltering: []uint64{},
+ DNSQueries: []uint64{},
+ ReplacedParental: []uint64{},
+ ReplacedSafebrowsing: []uint64{},
+ }
+ } else {
+ var ok bool
+ resp, ok = s.getData()
+
+ log.Debug("stats: prepared data in %v", time.Since(start))
+
+ if !ok {
+ httpError(r, w, http.StatusInternalServerError, "Couldn't get statistics data")
+
+ return
+ }
}
w.Header().Set("Content-Type", "application/json")
- err := json.NewEncoder(w).Encode(response)
+
+ err := json.NewEncoder(w).Encode(resp)
if err != nil {
httpError(r, w, http.StatusInternalServerError, "json encode: %s", err)
diff --git a/internal/stats/unit.go b/internal/stats/unit.go
index 968a2f66b21..8997da28868 100644
--- a/internal/stats/unit.go
+++ b/internal/stats/unit.go
@@ -147,7 +147,7 @@ func (s *statsCtx) Start() {
}
func checkInterval(days uint32) bool {
- return days == 1 || days == 7 || days == 30 || days == 90
+ return days == 0 || days == 1 || days == 7 || days == 30 || days == 90
}
func (s *statsCtx) dbOpen() bool {
@@ -251,7 +251,7 @@ func (s *statsCtx) periodicFlush() {
}
id := s.conf.UnitID()
- if ptr.id == id {
+ if ptr.id == id || s.conf.limit == 0 {
time.Sleep(time.Second)
continue
@@ -412,9 +412,11 @@ func convertTopSlice(a []countPair) []map[string]uint64 {
}
func (s *statsCtx) setLimit(limitDays int) {
- conf := *s.conf
- conf.limit = uint32(limitDays) * 24
- s.conf = &conf
+ s.conf.limit = uint32(limitDays) * 24
+ if limitDays == 0 {
+ s.clear()
+ }
+
log.Debug("stats: set limit: %d", limitDays)
}
@@ -488,6 +490,10 @@ func (s *statsCtx) getClientIP(ip net.IP) (clientIP net.IP) {
}
func (s *statsCtx) Update(e Entry) {
+ if s.conf.limit == 0 {
+ return
+ }
+
if e.Result == 0 ||
e.Result >= rLast ||
e.Domain == "" ||
@@ -695,6 +701,10 @@ func (s *statsCtx) getData() (statsResponse, bool) {
}
func (s *statsCtx) GetTopClientsIP(maxCount uint) []net.IP {
+ if s.conf.limit == 0 {
+ return nil
+ }
+
units, _ := s.loadUnits(s.conf.limit)
if units == nil {
return nil
diff --git a/openapi/CHANGELOG.md b/openapi/CHANGELOG.md
index 5b0d28c042e..7660cc27725 100644
--- a/openapi/CHANGELOG.md
+++ b/openapi/CHANGELOG.md
@@ -4,6 +4,11 @@
## v0.107: API changes
+### Disabling Statistics
+
+* The API `POST /control/stats_config` HTTP API allows disabling statistics by
+ setting `"interval"` to `0`.
+
### `POST /control/dhcp/reset_leases`
* The new `POST /control/dhcp/reset_leases` HTTP API allows removing all leases
diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml
index 313c739f723..7b145ed0f44 100644
--- a/openapi/openapi.yaml
+++ b/openapi/openapi.yaml
@@ -1630,8 +1630,16 @@
'description': 'Statistics configuration'
'properties':
'interval':
+ 'description': >
+ Time period to keep the data. `0` means that the statistics is
+ disabled.
+ 'enum':
+ - 0
+ - 1
+ - 7
+ - 30
+ - 90
'type': 'integer'
- 'description': 'Time period to keep data (1 | 7 | 30 | 90)'
'DhcpConfig':
'type': 'object'
'properties':