activity

package
v0.2.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 5, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

Documentation

Overview

Package activity estimates external API network activity with keyed, epoch-scoped pseudonyms instead of retaining raw request identifiers. The colocated HMAC key is the privacy boundary: anyone who compromises both the database and key can enumerate the IPv4 input space and recover the IPv4 addresses represented by retained buckets. Raw addresses and user agents otherwise exist only for request classification and bucket derivation.

Index

Constants

View Source
const (
	KindDay   = "day"
	KindMonth = "month"

	// DailyWindowDays is the length of the daily chart. It is deliberately
	// shorter than the 35 retained daily epochs so the newest end of the
	// chart can never be truncated by a prune that runs mid-render.
	DailyWindowDays = 31
)

Variables

View Source
var (
	ErrUnavailable = errors.New("activity estimates unavailable")
	ErrInvalidKey  = errors.New("activity hash key invalid")
	// ErrNoNetworkIdentity means the trust boundary refused to derive an
	// identity for this request — the fail-closed outcome, not a fault.
	ErrNoNetworkIdentity = errors.New("activity network identity not derivable")
)

Functions

func ExternalRequestAddress added in v0.1.15

func ExternalRequestAddress(r *http.Request) (netip.Addr, bool)

ExternalRequestAddress exposes the same fail-closed proxy boundary to private operational features that need to identify an internal machine. Callers must not log or publish the returned address.

Types

type Bucket

type Bucket struct {
	Kind   string
	Epoch  string
	Value  [16]byte
	Owner  bool
	SeenAt time.Time
}

Bucket is the complete persistence boundary. It intentionally has no raw address, header, route, or stable identifier field.

type Config

type Config struct {
	HashKeyHex       string
	QueueSize        int
	BatchSize        int
	SeenLimit        int
	FlushEvery       time.Duration
	MaintenanceEvery time.Duration
	DBTimeout        time.Duration
	Now              func() time.Time
	// contains filtered or unexported fields
}

Config exposes small test seams while production uses bounded defaults.

type Counts

type Counts struct {
	ExternalDAU int64
	ExternalMAU int64
	OwnerDAU    int64
	OwnerMAU    int64
	DaySeen     bool
	MonthSeen   bool
}

type DailyRaw

type DailyRaw struct {
	Days []DayCount
	// OldestEpoch is the oldest retained daily epoch, "" when none exists.
	OldestEpoch string
}

DailyRaw is the unshaped store result behind the daily chart.

type DailyWindow

type DailyWindow struct {
	Points     []DayPoint
	StartEpoch string
	Gaps       int
	Max        int64
	Collecting bool
}

DailyWindow is exactly DailyWindowDays points, oldest first.

func BuildDailyWindow

func BuildDailyWindow(now time.Time, raw DailyRaw) DailyWindow

BuildDailyWindow shapes stored epochs into a fixed-length chart that never invents a zero. Days older than the oldest retained epoch are marked BeforeCollection rather than counted as zero activity, and days at or after it that stored nothing are marked as gaps.

type DayCount

type DayCount struct {
	Epoch         string
	Count         int64
	OwnerExcluded int64
	Rows          int64
	Healthy       bool
}

DayCount is one stored daily epoch. Count excludes owner buckets; Rows includes them, so a day the operator alone touched is still provably collected rather than being reported as a gap.

type DayPoint

type DayPoint struct {
	Epoch            string
	Count            int64
	BeforeCollection bool
	Gap              bool
	HealthyZero      bool
}

DayPoint is one column of the daily chart. BeforeCollection marks a day older than anything retained — the chart cannot claim zero there, only silence. Gap marks a day inside the collected range that stored nothing, which is a real reporting gap and is shown as one.

type MaintenanceStore

type MaintenanceStore interface {
	MarkActivityHealthy(ctx context.Context, now time.Time) error
	PruneActivity(ctx context.Context, now time.Time) error
}

MaintenanceStore is deliberately narrower than Store. Server startup and periodic retention remain available even if collection or dashboard method wiring changes independently.

type Metrics

type Metrics struct {
	Counts    Counts
	Daily     DailyWindow
	Telemetry Telemetry
}

type Store

type Store interface {
	RecordActivity(ctx context.Context, buckets []Bucket) error
	ActivityCounts(ctx context.Context, dayEpoch, monthEpoch string) (Counts, error)
	// ActivityDaily reports the retained daily epochs inside the inclusive
	// [fromEpoch, toEpoch] range, plus the oldest daily epoch still retained
	// anywhere in the table. The oldest epoch may predate fromEpoch, which is
	// exactly how a full chart is distinguished from one whose left edge is
	// simply older than collection itself.
	ActivityDaily(ctx context.Context, fromEpoch, toEpoch string) (DailyRaw, error)
}

Store is optional and deliberately separate from serverstore.Store.

type Telemetry

type Telemetry struct {
	Dropped       uint64
	StoreFailures uint64
	Flushes       uint64
	Pending       int
}

Telemetry is the collector's own health. Flushes counts batches persisted successfully; StoreFailures counts persistence errors; Dropped counts observations that never reached storage, whether the queue was saturated or a flush failed. Every one of them makes the reported estimate a floor.

type Tracker

type Tracker struct {
	// contains filtered or unexported fields
}

func New

func New(ctx context.Context, store Store, cfg Config) *Tracker

New validates a dedicated 256-bit key. Store maintenance starts whenever a store is available, independently of whether collection is configured, so retention and daily health markers never depend on possession of the key.

func NewWithMaintenance

func NewWithMaintenance(ctx context.Context, store Store, maintenanceStore MaintenanceStore, cfg Config) *Tracker

NewWithMaintenance allows server wiring to retain maintenance even when the broader optional collection Store contract is unavailable.

func (*Tracker) Available

func (t *Tracker) Available() bool

func (*Tracker) Close

func (t *Tracker) Close(ctx context.Context) error

Close first closes the admission gate, then waits for the worker to drain both its staged batch and every observation that crossed the gate. A caller may retry Close with a fresh context after a timeout; Pending continues to include every item the worker has not yet persisted or recorded as dropped.

func (*Tracker) ConfigurationInvalid

func (t *Tracker) ConfigurationInvalid() bool

func (*Tracker) MarkOwner

func (t *Tracker) MarkOwner(ctx context.Context, r *http.Request, now time.Time) error

MarkOwner synchronously upserts the authenticated admin request's current buckets. A later queued non-owner observation cannot undo owner=true.

func (*Tracker) Metrics

func (t *Tracker) Metrics(ctx context.Context, now time.Time) (Metrics, error)

func (*Tracker) Telemetry

func (t *Tracker) Telemetry() Telemetry

func (*Tracker) Wrap

func (t *Tracker) Wrap(next http.Handler) http.Handler

Wrap records only meaningful, non-bot public API traffic. Derivation and a nonblocking enqueue are the only request-path work; storage can never delay or fail the wrapped response.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL