mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
Daily Active User Metrics (#3735)
* agent: add StatsReporter * Stabilize protoc
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
package metricscache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/retry"
|
||||
)
|
||||
|
||||
// Cache holds the template DAU cache.
|
||||
// The aggregation queries responsible for these values can take up to a minute
|
||||
// on large deployments. Even in small deployments, aggregation queries can
|
||||
// take a few hundred milliseconds, which would ruin page load times and
|
||||
// database performance if in the hot path.
|
||||
type Cache struct {
|
||||
database database.Store
|
||||
log slog.Logger
|
||||
|
||||
templateDAUResponses atomic.Pointer[map[string]codersdk.TemplateDAUsResponse]
|
||||
|
||||
doneCh chan struct{}
|
||||
cancel func()
|
||||
|
||||
interval time.Duration
|
||||
}
|
||||
|
||||
func New(db database.Store, log slog.Logger, interval time.Duration) *Cache {
|
||||
if interval <= 0 {
|
||||
interval = time.Hour
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
c := &Cache{
|
||||
database: db,
|
||||
log: log,
|
||||
doneCh: make(chan struct{}),
|
||||
cancel: cancel,
|
||||
interval: interval,
|
||||
}
|
||||
go c.run(ctx)
|
||||
return c
|
||||
}
|
||||
|
||||
func fillEmptyDays(rows []database.GetTemplateDAUsRow) []database.GetTemplateDAUsRow {
|
||||
var newRows []database.GetTemplateDAUsRow
|
||||
|
||||
for i, row := range rows {
|
||||
if i == 0 {
|
||||
newRows = append(newRows, row)
|
||||
continue
|
||||
}
|
||||
|
||||
last := rows[i-1]
|
||||
|
||||
const day = time.Hour * 24
|
||||
diff := row.Date.Sub(last.Date)
|
||||
for diff > day {
|
||||
if diff <= day {
|
||||
break
|
||||
}
|
||||
last.Date = last.Date.Add(day)
|
||||
last.Amount = 0
|
||||
newRows = append(newRows, last)
|
||||
diff -= day
|
||||
}
|
||||
|
||||
newRows = append(newRows, row)
|
||||
continue
|
||||
}
|
||||
|
||||
return newRows
|
||||
}
|
||||
|
||||
func (c *Cache) refresh(ctx context.Context) error {
|
||||
err := c.database.DeleteOldAgentStats(ctx)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("delete old stats: %w", err)
|
||||
}
|
||||
|
||||
templates, err := c.database.GetTemplates(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
templateDAUs := make(map[string]codersdk.TemplateDAUsResponse, len(templates))
|
||||
|
||||
for _, template := range templates {
|
||||
daus, err := c.database.GetTemplateDAUs(ctx, template.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var resp codersdk.TemplateDAUsResponse
|
||||
for _, ent := range fillEmptyDays(daus) {
|
||||
resp.Entries = append(resp.Entries, codersdk.DAUEntry{
|
||||
Date: ent.Date,
|
||||
Amount: int(ent.Amount),
|
||||
})
|
||||
}
|
||||
templateDAUs[template.ID.String()] = resp
|
||||
}
|
||||
|
||||
c.templateDAUResponses.Store(&templateDAUs)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cache) run(ctx context.Context) {
|
||||
defer close(c.doneCh)
|
||||
|
||||
ticker := time.NewTicker(c.interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
for r := retry.New(time.Millisecond*100, time.Minute); r.Wait(ctx); {
|
||||
start := time.Now()
|
||||
err := c.refresh(ctx)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
c.log.Error(ctx, "refresh", slog.Error(err))
|
||||
continue
|
||||
}
|
||||
c.log.Debug(
|
||||
ctx,
|
||||
"metrics refreshed",
|
||||
slog.F("took", time.Since(start)),
|
||||
slog.F("interval", c.interval),
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ticker.C:
|
||||
case <-c.doneCh:
|
||||
return
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) Close() error {
|
||||
c.cancel()
|
||||
<-c.doneCh
|
||||
return nil
|
||||
}
|
||||
|
||||
// TemplateDAUs returns an empty response if the template doesn't have users
|
||||
// or is loading for the first time.
|
||||
func (c *Cache) TemplateDAUs(id uuid.UUID) codersdk.TemplateDAUsResponse {
|
||||
m := c.templateDAUResponses.Load()
|
||||
if m == nil {
|
||||
// Data loading.
|
||||
return codersdk.TemplateDAUsResponse{}
|
||||
}
|
||||
|
||||
resp, ok := (*m)[id.String()]
|
||||
if !ok {
|
||||
// Probably no data.
|
||||
return codersdk.TemplateDAUsResponse{}
|
||||
}
|
||||
return resp
|
||||
}
|
||||
Reference in New Issue
Block a user