2019-08-22 16:34:58 +03:00
package stats
import (
"bytes"
"encoding/binary"
"encoding/gob"
"fmt"
2020-03-03 20:21:53 +03:00
"net"
2019-08-22 16:34:58 +03:00
"os"
"sort"
"sync"
2022-08-04 19:05:28 +03:00
"sync/atomic"
2019-08-22 16:34:58 +03:00
"time"
2022-08-04 19:05:28 +03:00
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
2021-05-24 17:28:11 +03:00
"github.com/AdguardTeam/golibs/errors"
2019-08-22 16:34:58 +03:00
"github.com/AdguardTeam/golibs/log"
2022-08-04 19:05:28 +03:00
"go.etcd.io/bbolt"
2019-08-22 16:34:58 +03:00
)
2021-02-11 17:55:37 +03:00
// TODO(a.garipov): Rewrite all of this. Add proper error handling and
// inspection. Improve logging. Decrease complexity.
2019-08-22 16:34:58 +03:00
const (
maxDomains = 100 // max number of top domains to store in file or return via Get()
maxClients = 100 // max number of top clients to store in file or return via Get()
)
2022-08-04 19:05:28 +03:00
// StatsCtx collects the statistics and flushes it to the database. Its default
// flushing interval is one hour.
//
// TODO(e.burkov): Use atomic.Pointer for accessing curr and db in go1.19.
type StatsCtx struct {
// currMu protects the current unit.
currMu * sync . Mutex
// curr is the actual statistics collection result.
curr * unit
// dbMu protects db.
dbMu * sync . Mutex
// db is the opened statistics database, if any.
db * bbolt . DB
// unitIDGen is the function that generates an identifier for the current
// unit. It's here for only testing purposes.
unitIDGen UnitIDGenFunc
// httpRegister is used to set HTTP handlers.
httpRegister aghhttp . RegisterFunc
// configModified is called whenever the configuration is modified via web
// interface.
configModified func ( )
2021-12-06 17:26:43 +03:00
2022-08-04 19:05:28 +03:00
// filename is the name of database file.
filename string
// limitHours is the maximum number of hours to collect statistics into the
// current unit.
limitHours uint32
2019-08-22 16:34:58 +03:00
}
2022-08-04 19:05:28 +03:00
// unit collects the statistics data for a specific period of time.
2019-08-22 16:34:58 +03:00
type unit struct {
2022-08-04 19:05:28 +03:00
// mu protects all the fields of a unit.
mu * sync . RWMutex
// id is the unique unit's identifier. It's set to an absolute hour number
// since the beginning of UNIX time by the default ID generating function.
id uint32
// nTotal stores the total number of requests.
nTotal uint64
// nResult stores the number of requests grouped by it's result.
nResult [ ] uint64
// timeSum stores the sum of processing time in milliseconds of each request
// written by the unit.
timeSum uint64
// domains stores the number of requests for each domain.
domains map [ string ] uint64
// blockedDomains stores the number of requests for each domain that has
// been blocked.
blockedDomains map [ string ] uint64
// clients stores the number of requests from each client.
clients map [ string ] uint64
}
// ongoing returns the current unit. It's safe for concurrent use.
//
// Note that the unit itself should be locked before accessing.
func ( s * StatsCtx ) ongoing ( ) ( u * unit ) {
s . currMu . Lock ( )
defer s . currMu . Unlock ( )
return s . curr
}
2019-08-22 16:34:58 +03:00
2022-08-04 19:05:28 +03:00
// swapCurrent swaps the current unit with another and returns it. It's safe
// for concurrent use.
func ( s * StatsCtx ) swapCurrent ( with * unit ) ( old * unit ) {
s . currMu . Lock ( )
defer s . currMu . Unlock ( )
2019-08-22 16:34:58 +03:00
2022-08-04 19:05:28 +03:00
old , s . curr = s . curr , with
return old
2019-08-22 16:34:58 +03:00
}
2022-08-04 19:05:28 +03:00
// database returns the database if it's opened. It's safe for concurrent use.
func ( s * StatsCtx ) database ( ) ( db * bbolt . DB ) {
s . dbMu . Lock ( )
defer s . dbMu . Unlock ( )
return s . db
}
// swapDatabase swaps the database with another one and returns it. It's safe
// for concurrent use.
func ( s * StatsCtx ) swapDatabase ( with * bbolt . DB ) ( old * bbolt . DB ) {
s . dbMu . Lock ( )
defer s . dbMu . Unlock ( )
old , s . db = s . db , with
return old
}
// countPair is a single name-number pair for deserializing statistics data into
// the database.
2019-08-22 16:34:58 +03:00
type countPair struct {
Name string
2019-09-10 17:59:10 +03:00
Count uint64
2019-08-22 16:34:58 +03:00
}
2022-08-04 19:05:28 +03:00
// unitDB is the structure for deserializing statistics data into the database.
2019-08-22 16:34:58 +03:00
type unitDB struct {
2022-08-04 19:05:28 +03:00
// NTotal is the total number of requests.
NTotal uint64
// NResult is the number of requests by the result's kind.
2019-09-10 17:59:10 +03:00
NResult [ ] uint64
2019-08-22 16:34:58 +03:00
2022-08-04 19:05:28 +03:00
// Domains is the number of requests for each domain name.
Domains [ ] countPair
// BlockedDomains is the number of requests blocked for each domain name.
2019-08-22 16:34:58 +03:00
BlockedDomains [ ] countPair
2022-08-04 19:05:28 +03:00
// Clients is the number of requests from each client.
Clients [ ] countPair
2019-08-22 16:34:58 +03:00
2022-08-04 19:05:28 +03:00
// TimeAvg is the average of processing times in milliseconds of all the
// requests in the unit.
TimeAvg uint32
2019-08-22 16:34:58 +03:00
}
2022-01-20 17:19:09 +03:00
// withRecovered turns the value recovered from panic if any into an error and
// combines it with the one pointed by orig. orig must be non-nil.
func withRecovered ( orig * error ) {
p := recover ( )
if p == nil {
return
}
var err error
switch p := p . ( type ) {
case error :
err = fmt . Errorf ( "panic: %w" , p )
default :
err = fmt . Errorf ( "panic: recovered value of type %[1]T: %[1]v" , p )
}
* orig = errors . WithDeferred ( * orig , err )
}
2022-08-04 19:05:28 +03:00
// isEnabled is a helper that check if the statistics collecting is enabled.
func ( s * StatsCtx ) isEnabled ( ) ( ok bool ) {
return atomic . LoadUint32 ( & s . limitHours ) != 0
}
// New creates s from conf and properly initializes it. Don't use s before
// calling it's Start method.
func New ( conf Config ) ( s * StatsCtx , err error ) {
2022-01-20 17:19:09 +03:00
defer withRecovered ( & err )
2022-08-04 19:05:28 +03:00
s = & StatsCtx {
currMu : & sync . Mutex { } ,
dbMu : & sync . Mutex { } ,
filename : conf . Filename ,
configModified : conf . ConfigModified ,
httpRegister : conf . HTTPRegister ,
2021-12-06 17:26:43 +03:00
}
2022-08-04 19:05:28 +03:00
if s . limitHours = conf . LimitDays * 24 ; ! checkInterval ( conf . LimitDays ) {
s . limitHours = 24
2019-09-25 15:36:09 +03:00
}
2022-08-04 19:05:28 +03:00
if s . unitIDGen = newUnitID ; conf . UnitID != nil {
s . unitIDGen = conf . UnitID
2019-08-22 16:34:58 +03:00
}
2022-08-04 19:05:28 +03:00
if err = s . dbOpen ( ) ; err != nil {
return nil , fmt . Errorf ( "opening database: %w" , err )
2019-08-22 16:34:58 +03:00
}
2022-08-04 19:05:28 +03:00
id := s . unitIDGen ( )
tx := beginTxn ( s . db , true )
2019-08-22 16:34:58 +03:00
var udb * unitDB
if tx != nil {
log . Tracef ( "Deleting old units..." )
2022-08-04 19:05:28 +03:00
firstID := id - s . limitHours - 1
2019-08-22 16:34:58 +03:00
unitDel := 0
2021-02-11 17:55:37 +03:00
2021-08-27 14:50:37 +03:00
err = tx . ForEach ( newBucketWalker ( tx , & unitDel , firstID ) )
2021-02-11 17:55:37 +03:00
if err != nil && ! errors . Is ( err , errStop ) {
log . Debug ( "stats: deleting units: %s" , err )
2019-08-22 16:34:58 +03:00
}
udb = s . loadUnitFromDB ( tx , id )
if unitDel != 0 {
s . commitTxn ( tx )
} else {
2021-02-11 17:55:37 +03:00
err = tx . Rollback ( )
if err != nil {
log . Debug ( "rolling back: %s" , err )
}
2019-08-22 16:34:58 +03:00
}
}
2022-08-04 19:05:28 +03:00
u := newUnit ( id )
// This use of deserialize is safe since the accessed unit has just been
// created.
u . deserialize ( udb )
s . curr = u
2019-08-22 16:34:58 +03:00
2021-02-11 17:55:37 +03:00
log . Debug ( "stats: initialized" )
return s , nil
2019-08-22 16:34:58 +03:00
}
2021-08-27 14:50:37 +03:00
// TODO(a.garipov): See if this is actually necessary. Looks like a rather
// bizarre solution.
const errStop errors . Error = "stop iteration"
// newBucketWalker returns a new bucket walker that deletes old units. The
// integer that unitDelPtr points to is incremented for every successful
// deletion. If the bucket isn't deleted, f returns errStop.
func newBucketWalker (
2022-08-04 19:05:28 +03:00
tx * bbolt . Tx ,
2021-08-27 14:50:37 +03:00
unitDelPtr * int ,
firstID uint32 ,
2022-08-04 19:05:28 +03:00
) ( f func ( name [ ] byte , b * bbolt . Bucket ) ( err error ) ) {
return func ( name [ ] byte , _ * bbolt . Bucket ) ( err error ) {
2021-08-27 14:50:37 +03:00
nameID , ok := unitNameToID ( name )
if ! ok || nameID < firstID {
err = tx . DeleteBucket ( name )
if err != nil {
log . Debug ( "stats: tx.DeleteBucket: %s" , err )
return nil
}
log . Debug ( "stats: deleted unit %d (name %x)" , nameID , name )
* unitDelPtr ++
return nil
}
return errStop
}
}
2022-08-04 19:05:28 +03:00
// Start makes s process the incoming data.
func ( s * StatsCtx ) Start ( ) {
2020-01-16 14:25:40 +03:00
s . initWeb ( )
go s . periodicFlush ( )
}
2022-08-04 19:05:28 +03:00
// checkInterval returns true if days is valid to be used as statistics
// retention interval. The valid values are 0, 1, 7, 30 and 90.
func checkInterval ( days uint32 ) ( ok bool ) {
2021-06-17 19:44:46 +03:00
return days == 0 || days == 1 || days == 7 || days == 30 || days == 90
2019-09-25 15:36:09 +03:00
}
2022-08-04 19:05:28 +03:00
// dbOpen returns an error if the database can't be opened from the specified
// file. It's safe for concurrent use.
func ( s * StatsCtx ) dbOpen ( ) ( err error ) {
2019-08-22 16:34:58 +03:00
log . Tracef ( "db.Open..." )
2022-08-04 19:05:28 +03:00
s . dbMu . Lock ( )
defer s . dbMu . Unlock ( )
s . db , err = bbolt . Open ( s . filename , 0 o644 , nil )
2019-08-22 16:34:58 +03:00
if err != nil {
2022-08-04 19:05:28 +03:00
log . Error ( "stats: open DB: %s: %s" , s . filename , err )
2019-12-09 14:13:39 +03:00
if err . Error ( ) == "invalid argument" {
2021-04-08 16:44:01 +03:00
log . Error ( "AdGuard Home cannot be initialized due to an incompatible file system.\nPlease read the explanation here: https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started#limitations" )
2019-12-09 14:13:39 +03:00
}
2019-08-22 16:34:58 +03:00
2022-08-04 19:05:28 +03:00
return err
}
2021-12-06 17:26:43 +03:00
2022-08-04 19:05:28 +03:00
log . Tracef ( "db.Open" )
2021-12-06 17:26:43 +03:00
2022-08-04 19:05:28 +03:00
return nil
2019-08-22 16:34:58 +03:00
}
2022-08-04 19:05:28 +03:00
// newUnitID is the default UnitIDGenFunc that generates the unique id hourly.
func newUnitID ( ) ( id uint32 ) {
const secsInHour = int64 ( time . Hour / time . Second )
return uint32 ( time . Now ( ) . Unix ( ) / secsInHour )
2019-08-22 16:34:58 +03:00
}
2022-08-04 19:05:28 +03:00
// newUnit allocates the new *unit.
func newUnit ( id uint32 ) ( u * unit ) {
return & unit {
mu : & sync . RWMutex { } ,
id : id ,
nResult : make ( [ ] uint64 , resultLast ) ,
domains : make ( map [ string ] uint64 ) ,
blockedDomains : make ( map [ string ] uint64 ) ,
clients : make ( map [ string ] uint64 ) ,
}
2019-08-22 16:34:58 +03:00
}
2022-08-04 19:05:28 +03:00
// beginTxn opens a new database transaction. If writable is true, the
// transaction will be opened for writing, and for reading otherwise. It
// returns nil if the transaction can't be created.
func beginTxn ( db * bbolt . DB , writable bool ) ( tx * bbolt . Tx ) {
2019-08-22 16:34:58 +03:00
if db == nil {
return nil
}
2022-08-04 19:05:28 +03:00
log . Tracef ( "opening a database transaction" )
tx , err := db . Begin ( writable )
2019-08-22 16:34:58 +03:00
if err != nil {
2022-08-04 19:05:28 +03:00
log . Error ( "stats: opening a transaction: %s" , err )
2019-08-22 16:34:58 +03:00
return nil
}
2022-08-04 19:05:28 +03:00
log . Tracef ( "transaction has been opened" )
2019-08-22 16:34:58 +03:00
return tx
}
2022-08-04 19:05:28 +03:00
// commitTxn applies the changes made in tx to the database.
func ( s * StatsCtx ) commitTxn ( tx * bbolt . Tx ) {
2019-08-22 16:34:58 +03:00
err := tx . Commit ( )
if err != nil {
2022-08-04 19:05:28 +03:00
log . Error ( "stats: committing a transaction: %s" , err )
2019-08-22 16:34:58 +03:00
return
}
2022-08-04 19:05:28 +03:00
log . Tracef ( "transaction has been committed" )
2019-08-22 16:34:58 +03:00
}
2021-08-27 14:50:37 +03:00
// bucketNameLen is the length of a bucket, a 64-bit unsigned integer.
//
// TODO(a.garipov): Find out why a 64-bit integer is used when IDs seem to
// always be 32 bits.
const bucketNameLen = 8
// idToUnitName converts a numerical ID into a database unit name.
func idToUnitName ( id uint32 ) ( name [ ] byte ) {
2022-08-04 19:05:28 +03:00
n := [ bucketNameLen ] byte { }
binary . BigEndian . PutUint64 ( n [ : ] , uint64 ( id ) )
2019-08-22 16:34:58 +03:00
2022-08-04 19:05:28 +03:00
return n [ : ]
2019-08-22 16:34:58 +03:00
}
2021-08-27 14:50:37 +03:00
// unitNameToID converts a database unit name into a numerical ID. ok is false
// if name is not a valid database unit name.
func unitNameToID ( name [ ] byte ) ( id uint32 , ok bool ) {
if len ( name ) < bucketNameLen {
return 0 , false
}
return uint32 ( binary . BigEndian . Uint64 ( name ) ) , true
2019-08-22 16:34:58 +03:00
}
// Flush the current unit to DB and delete an old unit when a new hour is started
2019-11-11 16:18:20 +03:00
// If a unit must be flushed:
// . lock DB
// . atomically set a new empty unit as the current one and get the old unit
// This is important to do it inside DB lock, so the reader won't get inconsistent results.
// . write the unit to DB
// . remove the stale unit from DB
// . unlock DB
2022-08-04 19:05:28 +03:00
func ( s * StatsCtx ) periodicFlush ( ) {
for ptr := s . ongoing ( ) ; ptr != nil ; ptr = s . ongoing ( ) {
id := s . unitIDGen ( )
// Access the unit's ID with atomic to avoid locking the whole unit.
if ! s . isEnabled ( ) || atomic . LoadUint32 ( & ptr . id ) == id {
2019-08-22 16:34:58 +03:00
time . Sleep ( time . Second )
2021-01-27 18:32:13 +03:00
2019-08-22 16:34:58 +03:00
continue
}
2022-08-04 19:05:28 +03:00
tx := beginTxn ( s . database ( ) , true )
2019-11-11 16:18:20 +03:00
2022-08-04 19:05:28 +03:00
nu := newUnit ( id )
u := s . swapCurrent ( nu )
udb := u . serialize ( )
2019-08-22 16:34:58 +03:00
if tx == nil {
continue
}
2021-01-27 18:32:13 +03:00
2022-08-04 19:05:28 +03:00
flushOK := flushUnitToDB ( tx , u . id , udb )
delOK := s . deleteUnit ( tx , id - atomic . LoadUint32 ( & s . limitHours ) )
if flushOK || delOK {
2019-08-22 16:34:58 +03:00
s . commitTxn ( tx )
} else {
_ = tx . Rollback ( )
}
}
2021-01-27 18:32:13 +03:00
2019-08-22 16:34:58 +03:00
log . Tracef ( "periodicFlush() exited" )
}
2022-08-04 19:05:28 +03:00
// deleteUnit removes the unit by it's id from the database the tx belongs to.
func ( s * StatsCtx ) deleteUnit ( tx * bbolt . Tx , id uint32 ) bool {
2021-08-27 14:50:37 +03:00
err := tx . DeleteBucket ( idToUnitName ( id ) )
2019-08-22 16:34:58 +03:00
if err != nil {
2021-02-11 17:55:37 +03:00
log . Tracef ( "stats: bolt DeleteBucket: %s" , err )
2019-08-22 16:34:58 +03:00
return false
}
2021-02-11 17:55:37 +03:00
log . Debug ( "stats: deleted unit %d" , id )
2019-08-22 16:34:58 +03:00
return true
}
2021-01-27 18:32:13 +03:00
func convertMapToSlice ( m map [ string ] uint64 , max int ) [ ] countPair {
2019-08-22 16:34:58 +03:00
a := [ ] countPair { }
for k , v := range m {
2022-08-04 19:05:28 +03:00
a = append ( a , countPair { Name : k , Count : v } )
2019-08-22 16:34:58 +03:00
}
less := func ( i , j int ) bool {
2020-11-06 12:15:08 +03:00
return a [ j ] . Count < a [ i ] . Count
2019-08-22 16:34:58 +03:00
}
sort . Slice ( a , less )
if max > len ( a ) {
max = len ( a )
}
return a [ : max ]
}
2021-01-27 18:32:13 +03:00
func convertSliceToMap ( a [ ] countPair ) map [ string ] uint64 {
2019-09-10 17:59:10 +03:00
m := map [ string ] uint64 { }
2019-08-22 16:34:58 +03:00
for _ , it := range a {
2019-09-10 17:59:10 +03:00
m [ it . Name ] = it . Count
2019-08-22 16:34:58 +03:00
}
return m
}
2022-08-04 19:05:28 +03:00
// serialize converts u to the *unitDB. It's safe for concurrent use.
func ( u * unit ) serialize ( ) ( udb * unitDB ) {
u . mu . RLock ( )
defer u . mu . RUnlock ( )
2020-11-06 12:15:08 +03:00
2022-08-04 19:05:28 +03:00
var timeAvg uint32 = 0
2019-08-22 16:34:58 +03:00
if u . nTotal != 0 {
2022-08-04 19:05:28 +03:00
timeAvg = uint32 ( u . timeSum / u . nTotal )
2019-08-22 16:34:58 +03:00
}
2020-11-06 12:15:08 +03:00
2022-08-04 19:05:28 +03:00
return & unitDB {
NTotal : u . nTotal ,
NResult : append ( [ ] uint64 { } , u . nResult ... ) ,
Domains : convertMapToSlice ( u . domains , maxDomains ) ,
BlockedDomains : convertMapToSlice ( u . blockedDomains , maxDomains ) ,
Clients : convertMapToSlice ( u . clients , maxClients ) ,
TimeAvg : timeAvg ,
}
2019-08-22 16:34:58 +03:00
}
2022-08-04 19:05:28 +03:00
// deserealize assigns the appropriate values from udb to u. u must not be nil.
// It's safe for concurrent use.
func ( u * unit ) deserialize ( udb * unitDB ) {
if udb == nil {
return
2019-08-22 16:34:58 +03:00
}
2019-09-10 18:04:43 +03:00
2022-08-04 19:05:28 +03:00
u . mu . Lock ( )
defer u . mu . Unlock ( )
u . nTotal = udb . NTotal
u . nResult = make ( [ ] uint64 , resultLast )
copy ( u . nResult , udb . NResult )
2021-01-27 18:32:13 +03:00
u . domains = convertSliceToMap ( udb . Domains )
u . blockedDomains = convertSliceToMap ( udb . BlockedDomains )
u . clients = convertSliceToMap ( udb . Clients )
2022-08-04 19:05:28 +03:00
u . timeSum = uint64 ( udb . TimeAvg ) * udb . NTotal
2019-08-22 16:34:58 +03:00
}
2022-08-04 19:05:28 +03:00
func flushUnitToDB ( tx * bbolt . Tx , id uint32 , udb * unitDB ) bool {
2019-08-22 16:34:58 +03:00
log . Tracef ( "Flushing unit %d" , id )
2021-08-27 14:50:37 +03:00
bkt , err := tx . CreateBucketIfNotExists ( idToUnitName ( id ) )
2019-08-22 16:34:58 +03:00
if err != nil {
log . Error ( "tx.CreateBucketIfNotExists: %s" , err )
return false
}
var buf bytes . Buffer
enc := gob . NewEncoder ( & buf )
err = enc . Encode ( udb )
if err != nil {
log . Error ( "gob.Encode: %s" , err )
return false
}
err = bkt . Put ( [ ] byte { 0 } , buf . Bytes ( ) )
if err != nil {
log . Error ( "bkt.Put: %s" , err )
return false
}
return true
}
2022-08-04 19:05:28 +03:00
func ( s * StatsCtx ) loadUnitFromDB ( tx * bbolt . Tx , id uint32 ) * unitDB {
2021-08-27 14:50:37 +03:00
bkt := tx . Bucket ( idToUnitName ( id ) )
2019-08-22 16:34:58 +03:00
if bkt == nil {
return nil
}
2019-10-23 16:48:00 +03:00
// log.Tracef("Loading unit %d", id)
2019-08-22 16:34:58 +03:00
var buf bytes . Buffer
buf . Write ( bkt . Get ( [ ] byte { 0 } ) )
dec := gob . NewDecoder ( & buf )
udb := unitDB { }
err := dec . Decode ( & udb )
if err != nil {
log . Error ( "gob Decode: %s" , err )
return nil
}
return & udb
}
2022-08-04 19:05:28 +03:00
func convertTopSlice ( a [ ] countPair ) ( m [ ] map [ string ] uint64 ) {
m = make ( [ ] map [ string ] uint64 , 0 , len ( a ) )
2019-08-22 16:34:58 +03:00
for _ , it := range a {
2022-08-04 19:05:28 +03:00
m = append ( m , map [ string ] uint64 { it . Name : it . Count } )
2019-08-22 16:34:58 +03:00
}
2022-08-04 19:05:28 +03:00
2019-08-22 16:34:58 +03:00
return m
}
2022-08-04 19:05:28 +03:00
func ( s * StatsCtx ) setLimit ( limitDays int ) {
atomic . StoreUint32 ( & s . limitHours , uint32 ( 24 * limitDays ) )
2021-06-17 19:44:46 +03:00
if limitDays == 0 {
s . clear ( )
}
2022-08-04 19:05:28 +03:00
log . Debug ( "stats: set limit: %d days" , limitDays )
2019-09-25 15:36:09 +03:00
}
2022-08-04 19:05:28 +03:00
func ( s * StatsCtx ) WriteDiskConfig ( dc * DiskConfig ) {
dc . Interval = atomic . LoadUint32 ( & s . limitHours ) / 24
2019-08-22 16:34:58 +03:00
}
2022-08-04 19:05:28 +03:00
func ( s * StatsCtx ) Close ( ) {
u := s . swapCurrent ( nil )
db := s . database ( )
if tx := beginTxn ( db , true ) ; tx != nil {
udb := u . serialize ( )
if flushUnitToDB ( tx , u . id , udb ) {
2019-08-22 16:34:58 +03:00
s . commitTxn ( tx )
} else {
_ = tx . Rollback ( )
}
}
2022-08-04 19:05:28 +03:00
if db != nil {
2019-08-22 16:34:58 +03:00
log . Tracef ( "db.Close..." )
2022-08-04 19:05:28 +03:00
_ = db . Close ( )
2019-08-22 16:34:58 +03:00
log . Tracef ( "db.Close" )
}
2021-02-11 17:55:37 +03:00
log . Debug ( "stats: closed" )
2019-08-22 16:34:58 +03:00
}
2019-09-25 15:36:09 +03:00
// Reset counters and clear database
2022-08-04 19:05:28 +03:00
func ( s * StatsCtx ) clear ( ) {
db := s . database ( )
tx := beginTxn ( db , true )
2019-08-22 16:34:58 +03:00
if tx != nil {
2022-08-04 19:05:28 +03:00
_ = s . swapDatabase ( nil )
2019-08-22 16:34:58 +03:00
_ = tx . Rollback ( )
// the active transactions can continue using database,
// but no new transactions will be opened
_ = db . Close ( )
log . Tracef ( "db.Close" )
// all active transactions are now closed
}
2022-08-04 19:05:28 +03:00
u := newUnit ( s . unitIDGen ( ) )
_ = s . swapCurrent ( u )
2019-08-22 16:34:58 +03:00
2022-08-04 19:05:28 +03:00
err := os . Remove ( s . filename )
2019-08-22 16:34:58 +03:00
if err != nil {
log . Error ( "os.Remove: %s" , err )
}
_ = s . dbOpen ( )
2021-02-11 17:55:37 +03:00
log . Debug ( "stats: cleared" )
2019-08-22 16:34:58 +03:00
}
2022-08-04 19:05:28 +03:00
func ( s * StatsCtx ) Update ( e Entry ) {
if ! s . isEnabled ( ) {
2021-06-17 19:44:46 +03:00
return
}
2019-08-22 16:34:58 +03:00
if e . Result == 0 ||
2022-08-04 19:05:28 +03:00
e . Result >= resultLast ||
2021-01-27 18:32:13 +03:00
e . Domain == "" ||
e . Client == "" {
2019-08-22 16:34:58 +03:00
return
}
2021-01-27 18:32:13 +03:00
clientID := e . Client
if ip := net . ParseIP ( clientID ) ; ip != nil {
clientID = ip . String ( )
}
2019-08-22 16:34:58 +03:00
2022-08-04 19:05:28 +03:00
u := s . ongoing ( )
if u == nil {
return
}
2021-01-27 18:32:13 +03:00
2022-08-04 19:05:28 +03:00
u . mu . Lock ( )
defer u . mu . Unlock ( )
2019-08-22 16:34:58 +03:00
u . nResult [ e . Result ] ++
if e . Result == RNotFiltered {
u . domains [ e . Domain ] ++
} else {
u . blockedDomains [ e . Domain ] ++
}
2021-01-27 18:32:13 +03:00
u . clients [ clientID ] ++
2019-09-10 17:59:10 +03:00
u . timeSum += uint64 ( e . Time )
2019-08-22 16:34:58 +03:00
u . nTotal ++
}
2022-08-04 19:05:28 +03:00
func ( s * StatsCtx ) loadUnits ( limit uint32 ) ( [ ] * unitDB , uint32 ) {
tx := beginTxn ( s . database ( ) , false )
2019-10-07 15:55:09 +03:00
if tx == nil {
2019-11-11 16:18:20 +03:00
return nil , 0
2019-10-07 15:55:09 +03:00
}
2021-12-06 17:26:43 +03:00
cur := s . ongoing ( )
2022-08-04 19:05:28 +03:00
var curID uint32
if cur != nil {
curID = atomic . LoadUint32 ( & cur . id )
} else {
curID = s . unitIDGen ( )
}
2019-11-11 16:18:20 +03:00
2020-11-06 12:15:08 +03:00
// Per-hour units.
units := [ ] * unitDB { }
2019-12-11 12:38:58 +03:00
firstID := curID - limit + 1
2019-11-11 16:18:20 +03:00
for i := firstID ; i != curID ; i ++ {
2019-10-07 15:55:09 +03:00
u := s . loadUnitFromDB ( tx , i )
if u == nil {
u = & unitDB { }
2022-08-04 19:05:28 +03:00
u . NResult = make ( [ ] uint64 , resultLast )
2019-10-07 15:55:09 +03:00
}
units = append ( units , u )
}
_ = tx . Rollback ( )
2022-08-04 19:05:28 +03:00
if cur != nil {
units = append ( units , cur . serialize ( ) )
}
2019-10-07 15:55:09 +03:00
2019-12-11 12:38:58 +03:00
if len ( units ) != int ( limit ) {
log . Fatalf ( "len(units) != limit: %d %d" , len ( units ) , limit )
2019-10-07 15:55:09 +03:00
}
2019-11-11 16:18:20 +03:00
return units , firstID
2019-10-07 15:55:09 +03:00
}
2021-02-09 19:38:31 +03:00
// numsGetter is a signature for statsCollector argument.
type numsGetter func ( u * unitDB ) ( num uint64 )
// statsCollector collects statisctics for the given *unitDB slice by specified
// timeUnit using ng to retrieve data.
func statsCollector ( units [ ] * unitDB , firstID uint32 , timeUnit TimeUnit , ng numsGetter ) ( nums [ ] uint64 ) {
if timeUnit == Hours {
for _ , u := range units {
nums = append ( nums , ng ( u ) )
}
} else {
// Per time unit counters: 720 hours may span 31 days, so we
// skip data for the first day in this case.
// align_ceil(24)
firstDayID := ( firstID + 24 - 1 ) / 24 * 24
var sum uint64
id := firstDayID
nextDayID := firstDayID + 24
for i := int ( firstDayID - firstID ) ; i != len ( units ) ; i ++ {
sum += ng ( units [ i ] )
if id == nextDayID {
nums = append ( nums , sum )
sum = 0
nextDayID += 24
}
id ++
}
if id <= nextDayID {
nums = append ( nums , sum )
}
}
return nums
}
// pairsGetter is a signature for topsCollector argument.
type pairsGetter func ( u * unitDB ) ( pairs [ ] countPair )
2022-08-04 19:05:28 +03:00
// topsCollector collects statistics about highest values from the given *unitDB
2021-02-09 19:38:31 +03:00
// slice using pg to retrieve data.
func topsCollector ( units [ ] * unitDB , max int , pg pairsGetter ) [ ] map [ string ] uint64 {
m := map [ string ] uint64 { }
for _ , u := range units {
2022-08-04 19:05:28 +03:00
for _ , cp := range pg ( u ) {
m [ cp . Name ] += cp . Count
2021-02-09 19:38:31 +03:00
}
}
a2 := convertMapToSlice ( m , max )
return convertTopSlice ( a2 )
}
2019-08-22 16:34:58 +03:00
/ * Algorithm :
. Prepare array of N units , where N is the value of "limit" configuration setting
. Load data for the most recent units from file
If a unit with required ID doesn ' t exist , just add an empty unit
. Get data for the current unit
. Process data from the units and prepare an output map object :
* per time unit counters :
* DNS - queries / time - unit
* blocked / time - unit
* safebrowsing - blocked / time - unit
* parental - blocked / time - unit
If time - unit is an hour , just add values from each unit to an array .
If time - unit is a day , aggregate per - hour data into days .
* top counters :
* queries / domain
* queries / blocked - domain
* queries / client
To get these values we first sum up data for all units into a single map .
Then we get the pairs with the highest numbers ( the values are sorted in descending order )
* total counters :
* DNS - queries
* blocked
* safebrowsing - blocked
* safesearch - blocked
* parental - blocked
These values are just the sum of data for all units .
* /
2022-08-04 19:05:28 +03:00
func ( s * StatsCtx ) getData ( ) ( statsResponse , bool ) {
limit := atomic . LoadUint32 ( & s . limitHours )
if limit == 0 {
return statsResponse {
TimeUnits : "days" ,
TopBlocked : [ ] topAddrs { } ,
TopClients : [ ] topAddrs { } ,
TopQueried : [ ] topAddrs { } ,
BlockedFiltering : [ ] uint64 { } ,
DNSQueries : [ ] uint64 { } ,
ReplacedParental : [ ] uint64 { } ,
ReplacedSafebrowsing : [ ] uint64 { } ,
} , true
}
2019-12-11 12:38:58 +03:00
timeUnit := Hours
if limit / 24 > 7 {
timeUnit = Days
}
2019-08-22 16:34:58 +03:00
2019-12-11 12:38:58 +03:00
units , firstID := s . loadUnits ( limit )
2019-10-07 15:55:09 +03:00
if units == nil {
2021-01-21 19:55:41 +03:00
return statsResponse { } , false
2019-08-22 16:34:58 +03:00
}
2021-02-09 19:38:31 +03:00
dnsQueries := statsCollector ( units , firstID , timeUnit , func ( u * unitDB ) ( num uint64 ) { return u . NTotal } )
2020-11-20 17:32:41 +03:00
if timeUnit != Hours && len ( dnsQueries ) != int ( limit / 24 ) {
log . Fatalf ( "len(dnsQueries) != limit: %d %d" , len ( dnsQueries ) , limit )
2019-08-22 16:34:58 +03:00
}
2021-01-21 19:55:41 +03:00
data := statsResponse {
DNSQueries : dnsQueries ,
2021-02-09 19:38:31 +03:00
BlockedFiltering : statsCollector ( units , firstID , timeUnit , func ( u * unitDB ) ( num uint64 ) { return u . NResult [ RFiltered ] } ) ,
ReplacedSafebrowsing : statsCollector ( units , firstID , timeUnit , func ( u * unitDB ) ( num uint64 ) { return u . NResult [ RSafeBrowsing ] } ) ,
ReplacedParental : statsCollector ( units , firstID , timeUnit , func ( u * unitDB ) ( num uint64 ) { return u . NResult [ RParental ] } ) ,
TopQueried : topsCollector ( units , maxDomains , func ( u * unitDB ) ( pairs [ ] countPair ) { return u . Domains } ) ,
TopBlocked : topsCollector ( units , maxDomains , func ( u * unitDB ) ( pairs [ ] countPair ) { return u . BlockedDomains } ) ,
TopClients : topsCollector ( units , maxClients , func ( u * unitDB ) ( pairs [ ] countPair ) { return u . Clients } ) ,
2019-08-22 16:34:58 +03:00
}
2021-02-09 19:38:31 +03:00
// Total counters:
sum := unitDB {
2022-08-04 19:05:28 +03:00
NResult : make ( [ ] uint64 , resultLast ) ,
2021-02-09 19:38:31 +03:00
}
2019-08-22 16:34:58 +03:00
timeN := 0
for _ , u := range units {
sum . NTotal += u . NTotal
sum . TimeAvg += u . TimeAvg
if u . TimeAvg != 0 {
timeN ++
}
sum . NResult [ RFiltered ] += u . NResult [ RFiltered ]
sum . NResult [ RSafeBrowsing ] += u . NResult [ RSafeBrowsing ]
sum . NResult [ RSafeSearch ] += u . NResult [ RSafeSearch ]
sum . NResult [ RParental ] += u . NResult [ RParental ]
}
2021-01-21 19:55:41 +03:00
data . NumDNSQueries = sum . NTotal
data . NumBlockedFiltering = sum . NResult [ RFiltered ]
data . NumReplacedSafebrowsing = sum . NResult [ RSafeBrowsing ]
data . NumReplacedSafesearch = sum . NResult [ RSafeSearch ]
data . NumReplacedParental = sum . NResult [ RParental ]
2019-08-22 16:34:58 +03:00
if timeN != 0 {
2021-01-21 19:55:41 +03:00
data . AvgProcessingTime = float64 ( sum . TimeAvg / uint32 ( timeN ) ) / 1000000
2019-08-22 16:34:58 +03:00
}
2021-01-21 19:55:41 +03:00
data . TimeUnits = "hours"
2019-08-22 16:34:58 +03:00
if timeUnit == Days {
2021-01-21 19:55:41 +03:00
data . TimeUnits = "days"
2019-08-22 16:34:58 +03:00
}
2021-01-21 19:55:41 +03:00
return data , true
2019-08-22 16:34:58 +03:00
}
2019-10-07 15:56:33 +03:00
2022-08-04 19:05:28 +03:00
func ( s * StatsCtx ) GetTopClientsIP ( maxCount uint ) [ ] net . IP {
if ! s . isEnabled ( ) {
2021-06-17 19:44:46 +03:00
return nil
}
2022-08-04 19:05:28 +03:00
units , _ := s . loadUnits ( atomic . LoadUint32 ( & s . limitHours ) )
2019-10-07 15:56:33 +03:00
if units == nil {
return nil
}
// top clients
m := map [ string ] uint64 { }
for _ , u := range units {
for _ , it := range u . Clients {
m [ it . Name ] += it . Count
}
}
2021-01-27 18:32:13 +03:00
a := convertMapToSlice ( m , int ( maxCount ) )
2021-01-20 17:27:53 +03:00
d := [ ] net . IP { }
2019-10-07 15:56:33 +03:00
for _ , it := range a {
2021-06-29 15:53:28 +03:00
ip := net . ParseIP ( it . Name )
if ip != nil {
d = append ( d , ip )
}
2019-10-07 15:56:33 +03:00
}
return d
}