169 lines
4.3 KiB
Go
Raw Permalink Normal View History

2022-08-26 14:18:35 +03:00
package rulestat
import (
"bytes"
"context"
"encoding/json"
"fmt"
2024-10-14 17:44:24 +03:00
"log/slog"
2022-08-26 14:18:35 +03:00
"net/http"
"net/url"
"sync"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/agdhttp"
2024-01-04 19:22:32 +03:00
"github.com/AdguardTeam/AdGuardDNS/internal/agdservice"
2024-06-07 14:27:46 +03:00
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
2024-12-05 14:19:25 +03:00
"github.com/AdguardTeam/AdGuardDNS/internal/filter"
2022-08-26 14:18:35 +03:00
"github.com/AdguardTeam/AdGuardDNS/internal/metrics"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/netutil"
)
2024-06-07 14:27:46 +03:00
// statFilterListLegacyID is the ID of the filtering rule list for which we
2022-08-26 14:18:35 +03:00
// collect statistics, as understood and accepted by the current backend. This
// is a temporary restriction.
//
// TODO(ameshkov): Consider making the backend accept the current IDs.
2024-12-05 14:19:25 +03:00
const statFilterListLegacyID filter.ID = "15"
2022-08-26 14:18:35 +03:00
// HTTP is the filtering rule statistics collector that uploads the statistics
// to the given URL when it's refreshed.
//
// TODO(a.garipov): Add tests.
type HTTP struct {
2024-10-14 17:44:24 +03:00
logger *slog.Logger
2024-06-07 14:27:46 +03:00
url *url.URL
http *agdhttp.Client
errColl errcoll.Interface
2022-08-26 14:18:35 +03:00
// mu protects stats and recordedHits.
mu *sync.Mutex
stats statsSet
recordedHits int64
}
// statsSet is an alias for the stats set type.
2024-12-05 14:19:25 +03:00
type statsSet = map[filter.ID]map[filter.RuleText]uint64
2022-08-26 14:18:35 +03:00
// HTTPConfig is the configuration structure for the filtering rule statistics
2024-10-14 17:44:24 +03:00
// collector that uploads the statistics to a URL. All fields must not be nil.
2022-08-26 14:18:35 +03:00
type HTTPConfig struct {
2024-10-14 17:44:24 +03:00
// Logger is used for logging the operation of the statistics collector.
Logger *slog.Logger
2024-06-07 14:27:46 +03:00
// ErrColl is used to collect errors during refreshes.
ErrColl errcoll.Interface
2022-08-26 14:18:35 +03:00
// URL is the URL to which the statistics is uploaded.
URL *url.URL
}
// NewHTTP returns a new statistics collector with HTTP upload.
func NewHTTP(c *HTTPConfig) (s *HTTP) {
return &HTTP{
2024-10-14 17:44:24 +03:00
logger: c.Logger,
url: netutil.CloneURL(c.URL),
2022-08-26 14:18:35 +03:00
http: agdhttp.NewClient(&agdhttp.ClientConfig{
// TODO(ameshkov): Consider making configurable.
Timeout: 30 * time.Second,
}),
2024-06-07 14:27:46 +03:00
errColl: c.ErrColl,
mu: &sync.Mutex{},
stats: statsSet{},
2022-08-26 14:18:35 +03:00
}
}
// type check
var _ Interface = (*HTTP)(nil)
// Collect implements the Interface interface for *HTTP.
2024-12-05 14:19:25 +03:00
func (s *HTTP) Collect(_ context.Context, id filter.ID, text filter.RuleText) {
if id != filter.IDAdGuardDNS {
2022-08-26 14:18:35 +03:00
return
}
2024-06-07 14:27:46 +03:00
id = statFilterListLegacyID
2022-08-26 14:18:35 +03:00
s.mu.Lock()
defer s.mu.Unlock()
s.recordedHits++
metrics.RuleStatCacheSize.Set(float64(s.recordedHits))
texts := s.stats[id]
if texts != nil {
texts[text]++
return
}
2024-12-05 14:19:25 +03:00
s.stats[id] = map[filter.RuleText]uint64{
2022-08-26 14:18:35 +03:00
text: 1,
}
}
// type check
2024-01-04 19:22:32 +03:00
var _ agdservice.Refresher = (*HTTP)(nil)
2022-08-26 14:18:35 +03:00
2024-01-04 19:22:32 +03:00
// Refresh implements the [agdservice.Refresher] interface for *HTTP. It
// uploads the collected statistics to s.u and starts collecting a new set of
// statistics.
2022-08-26 14:18:35 +03:00
func (s *HTTP) Refresh(ctx context.Context) (err error) {
2024-10-14 17:44:24 +03:00
s.logger.InfoContext(ctx, "refresh started")
defer s.logger.InfoContext(ctx, "refresh finished")
2024-06-07 14:27:46 +03:00
2022-08-26 14:18:35 +03:00
err = s.refresh(ctx)
2024-06-07 14:27:46 +03:00
if err != nil {
2024-10-14 17:44:24 +03:00
errcoll.Collect(ctx, s.errColl, s.logger, "uploading rulestat", err)
2024-06-07 14:27:46 +03:00
metrics.SetStatusGauge(metrics.RuleStatUploadStatus, err)
2022-08-26 14:18:35 +03:00
2024-06-07 14:27:46 +03:00
return err
2022-08-26 14:18:35 +03:00
}
2024-06-07 14:27:46 +03:00
metrics.RuleStatUploadTimestamp.SetToCurrentTime()
metrics.SetStatusGauge(metrics.RuleStatUploadStatus, nil)
2022-08-26 14:18:35 +03:00
2024-06-07 14:27:46 +03:00
return nil
2022-08-26 14:18:35 +03:00
}
// refresh uploads the collected statistics and resets the collected stats.
func (s *HTTP) refresh(ctx context.Context) (err error) {
stats := s.replaceStats()
req := &filtersReq{
Filters: stats,
}
b, err := json.Marshal(req)
if err != nil {
return fmt.Errorf("encoding filter stats: %w", err)
}
httpResp, err := s.http.Post(ctx, s.url, agdhttp.HdrValApplicationJSON, bytes.NewReader(b))
if err != nil {
return fmt.Errorf("uploading filter stats: %w", err)
}
defer func() { err = errors.WithDeferred(err, httpResp.Body.Close()) }()
2024-06-07 14:27:46 +03:00
// Don't wrap the error, because it's informative enough as is.
return agdhttp.CheckStatus(httpResp, http.StatusOK)
2022-08-26 14:18:35 +03:00
}
// replaceStats replaced the current stats of s with a new set and returns the
// previous one.
func (s *HTTP) replaceStats() (prev statsSet) {
s.mu.Lock()
defer s.mu.Unlock()
prev, s.stats = s.stats, statsSet{}
s.recordedHits = 0
return prev
}
// filtersReq is the JSON filtering rule list statistics request structure.
type filtersReq struct {
Filters statsSet `json:"filters"`
}