metrics

package
v0.0.3 Latest Latest
Warning

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

Go to latest
Published: Jul 8, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package metrics provides domain-level analytics for r3 CRUD repositories.

It works as a decorator around any r3.CRUD[T, ID] implementation, recording configurable metrics for every CRUD operation. The decorator is transparent: the wrapped repository still satisfies r3.CRUD[T, ID].

"Everything is a R3po" -- metric records are themselves r3 entities, stored via any r3.CRUD[MetricRecord, string]. The metrics feature has zero knowledge of storage backends. You can use the same driver (SQL, GORM, MongoDB, etc.) that you use for your entities, or a completely different one.

Key features:

  • 10 built-in collectors covering common CRUD analytics (action counts, latency, popularity, error rates, filter analysis, field changes, lifecycle, patch size)
  • Extensible Collector[T, ID] interface for custom metrics
  • Three-layer label system: core (auto), context, and entity labels
  • Time bucketing for efficient aggregation (minutely to monthly)
  • Aggregator with Count, Sum, Avg, TopN, TimeSeries, GroupBy
  • AggregationPusher interface for server-side aggregation in capable backends
  • Retention policies (age-based TTL, record count limits)
  • Rollup/compaction of old fine-grained records into coarser summaries
  • Async mode for zero-latency impact on the hot path
  • r3.Actor context for automatic actor attribution

Aggregation

The Aggregator performs in-memory aggregation by default. If the underlying store also implements AggregationPusher, the Aggregator automatically delegates to server-side aggregation for better performance at scale.

Retention and Rollup

RetentionEnforcer deletes old metric records based on age (MaxAge) or count (MaxRecords). RollupExecutor compacts old fine-grained records into coarser time-bucket summaries, preserving label dimensions and annotating summaries with _rollup metadata labels.

Both retention and rollup delete records one-by-one via the r3.CRUD Delete method. For large datasets this may be slow; a future BatchDelete interface could optimize bulk deletions for capable backends.

The RollupExecutor compacts all old records for a given type regardless of their original bucket granularity. RollupPolicy.SourceBucket is recorded as provenance metadata only. To selectively compact only records of a specific granularity, filter them at the query level before rollup.

RetentionEnforcer.Start and RollupExecutor run on-demand or via a ticker-based background loop. The background loop fires after the first interval elapses, not immediately on start.

Usage

metricsStore := r3pgx.NewPgxCRUD[metrics.MetricRecord, string](db)

repo := metrics.WithMetrics[Order, int64](
    innerRepo,
    metricsStore,
    metrics.WithIDFunc[Order, int64](func(o Order) int64 { return o.ID }),
    metrics.WithCollectors[Order, int64](
        metrics.CRUDActionCollector[Order, int64](),
        metrics.PopularityCollector[Order, int64](),
        metrics.LatencyCollector[Order, int64](),
    ),
    metrics.WithLabelers[Order, int64](
        func(o Order) metrics.Labels { return metrics.Labels{"order_type": o.Type} },
    ),
    metrics.WithAsync[Order, int64](),
)

// Query metrics:
agg := metrics.NewAggregator(metricsStore)
count, _ := agg.Count(ctx, "orders", "crud.action", metrics.LastMonth(),
    metrics.WithLabel("operation", "create"))
top, _ := agg.TopN(ctx, "orders", "entity.popularity", metrics.LastWeek(), 10)

Index

Constants

View Source
const (
	// MetricEntityFieldChange tracks which fields change most often on updates.
	MetricEntityFieldChange = "entity.field_change"

	// MetricEntityLifecycle tracks the duration from creation to deletion.
	MetricEntityLifecycle = "entity.lifecycle"

	// MetricEntityPatchSize tracks how many fields are patched at once.
	MetricEntityPatchSize = "entity.patch_size"
)

Metric names for entity-level collectors.

View Source
const (
	// MetricListFilter tracks which fields users filter by in List queries.
	MetricListFilter = "crud.list.filter"

	// MetricListResultSize tracks how many results each List query returns.
	MetricListResultSize = "crud.list.result_size"

	// MetricListTotalCount tracks the total matching count from List queries (before pagination).
	MetricListTotalCount = "crud.list.total_count"
)

Metric names for list-related collectors.

View Source
const MetricCRUDAction = "crud.action"

MetricCRUDAction is the metric name for the universal CRUD action counter. Every CRUD operation increments this metric by 1. The "operation" core label distinguishes create/get/list/update/patch/delete.

View Source
const MetricCRUDError = "crud.error"

MetricCRUDError is the metric name for failed operation tracking.

View Source
const MetricCRUDLatency = "crud.action.latency"

MetricCRUDLatency is the metric name for operation duration tracking. Value is the duration in milliseconds.

View Source
const MetricEntityPopularity = "entity.popularity"

MetricEntityPopularity is the metric name for entity access scoring. Incremented when an entity is accessed via Get or appears in List results.

Variables

View Source
var (
	// ErrNoMetrics is returned when querying metrics for an entity with no recorded data.
	ErrNoMetrics = errors.New("r3metrics: no metrics found")

	// ErrIDFuncRequired is returned when a collector requires IDFunc but it's not set.
	ErrIDFuncRequired = errors.New("r3metrics: IDFunc is required for entity-level metrics; use WithIDFunc option")
)

Sentinel errors for metrics operations.

Functions

func ComputeBucket

func ComputeBucket(t time.Time, size BucketSize) string

ComputeBucket truncates a timestamp to the given bucket boundary and returns it as an ISO8601/RFC3339 string. Truncation is delegated to the years library so the boundary semantics stay consistent across the codebase.

func QueryByBucket

func QueryByBucket(recordType, metricName, bucket string) r3.Query

QueryByBucket builds a Query that retrieves metrics for a specific time bucket.

func QueryByEntity

func QueryByEntity(recordType, recordID string, tr TimeRange) r3.Query

QueryByEntity builds a Query that retrieves all metrics for a specific entity instance.

func QueryByEntityMetric

func QueryByEntityMetric(recordType, recordID, metricName string, tr TimeRange) r3.Query

QueryByEntityMetric builds a Query for a specific metric on a specific entity.

func QueryByMetric

func QueryByMetric(recordType, metricName string, tr TimeRange) r3.Query

QueryByMetric builds a Query that retrieves a specific metric for a given entity type.

func QueryByType

func QueryByType(recordType string, tr TimeRange) r3.Query

QueryByType builds a Query that retrieves all metrics for a given entity type within a time range, sorted by created_at descending.

Types

type AggregationPusher

type AggregationPusher interface {
	// PushCount returns the count of metric records matching the given criteria.
	// The labels parameter is optional — if non-nil, only records with those labels are counted.
	PushCount(ctx context.Context, recordType, metricName string, tr TimeRange, labels Labels) (int64, error)

	// PushSum returns the sum of metric values matching the given criteria.
	PushSum(ctx context.Context, recordType, metricName string, tr TimeRange, labels Labels) (float64, error)

	// PushAvg returns the average of metric values matching the given criteria.
	PushAvg(ctx context.Context, recordType, metricName string, tr TimeRange, labels Labels) (float64, error)

	// PushTopN returns the top N entities by summed metric value.
	PushTopN(
		ctx context.Context,
		recordType, metricName string,
		tr TimeRange,
		n int,
		labels Labels,
	) ([]RankedEntity, error)

	// PushTimeSeries returns metric values grouped by time bucket.
	PushTimeSeries(
		ctx context.Context,
		recordType, metricName string,
		tr TimeRange,
		bucket BucketSize,
		labels Labels,
	) ([]BucketValue, error)

	// PushGroupBy returns metric values grouped by a label key.
	PushGroupBy(
		ctx context.Context,
		recordType, metricName string,
		tr TimeRange,
		labelKey string,
		labels Labels,
	) (map[string]float64, error)
}

AggregationPusher is an optional interface that metric stores can implement to provide server-side aggregation instead of in-memory computation.

Backends like Elasticsearch, ClickHouse, or TimescaleDB have native aggregation capabilities that are far more efficient than fetching all records and computing in Go. If a store implements AggregationPusher, the Aggregator will delegate to these methods instead of performing in-memory aggregation.

This is a pure opt-in: existing r3.CRUD[MetricRecord, string] stores continue to work with the in-memory Aggregator. Only stores that additionally implement this interface get the server-side performance benefit.

Usage:

// A custom store implementing both r3.CRUD and AggregationPusher:
type ClickHouseMetricStore struct { ... }
func (s *ClickHouseMetricStore) PushCount(...) (int64, error) { ... }
func (s *ClickHouseMetricStore) PushSum(...)   (float64, error) { ... }
// etc.

agg := metrics.NewAggregator(clickhouseStore)
// Aggregator automatically detects the AggregationPusher and delegates.

type Aggregator

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

Aggregator provides convenience methods for querying and computing metric aggregations. It works with any r3.Querier for MetricRecord — only read access is needed.

If the underlying store also implements AggregationPusher, the Aggregator automatically delegates to server-side aggregation for much better performance at scale. Otherwise, aggregation is done in-memory after fetching records.

func NewAggregator

func NewAggregator(store r3.Querier[MetricRecord, string]) *Aggregator

NewAggregator creates a new Aggregator using the given metric store. Only r3.Querier is required — the Aggregator never writes to the store. If the store also implements AggregationPusher, server-side aggregation is automatically enabled.

func (*Aggregator) Avg

func (a *Aggregator) Avg(
	ctx context.Context,
	recordType, metricName string,
	tr TimeRange,
	opts ...QueryOption,
) (float64, error)

Avg returns the average metric value matching the query.

Example: "Average latency of user creates?"

agg.Avg(ctx, "users", "crud.action.latency", LastWeek(), WithLabel("operation", "create"))

func (*Aggregator) Count

func (a *Aggregator) Count(
	ctx context.Context,
	recordType, metricName string,
	tr TimeRange,
	opts ...QueryOption,
) (int64, error)

Count returns the total count of metric records matching the query. For counter metrics (value=1), this equals the sum.

Example: "How many users were created this month?"

agg.Count(ctx, "users", "crud.action", LastMonth(), WithLabel("operation", "create"))

func (*Aggregator) GroupBy

func (a *Aggregator) GroupBy(
	ctx context.Context,
	recordType, metricName string,
	tr TimeRange,
	labelKey string,
	opts ...QueryOption,
) (map[string]float64, error)

GroupBy returns metric values grouped by a label key.

Example: "CRUD action breakdown for orders?"

agg.GroupBy(ctx, "orders", "crud.action", LastMonth(), "operation")
// -> {"create": 1247, "get": 8391, "list": 2100, ...}

func (*Aggregator) Sum

func (a *Aggregator) Sum(
	ctx context.Context,
	recordType, metricName string,
	tr TimeRange,
	opts ...QueryOption,
) (float64, error)

Sum returns the sum of metric values matching the query.

Example: "Total latency of all user list operations this week?"

agg.Sum(ctx, "users", "crud.action.latency", LastWeek(), WithLabel("operation", "list"))

func (*Aggregator) TimeSeries

func (a *Aggregator) TimeSeries(
	ctx context.Context,
	recordType, metricName string,
	tr TimeRange,
	bucket BucketSize,
	opts ...QueryOption,
) ([]BucketValue, error)

TimeSeries returns metric values grouped by time bucket.

Example: "Daily user creation count for the last 30 days?"

agg.TimeSeries(ctx, "users", "crud.action", Last30Days(), BucketDaily, WithLabel("operation", "create"))

func (*Aggregator) TopN

func (a *Aggregator) TopN(
	ctx context.Context,
	recordType, metricName string,
	tr TimeRange,
	n int,
	opts ...QueryOption,
) ([]RankedEntity, error)

TopN returns the N entities with the highest summed metric value.

Example: "Top 10 most queried users this month?"

agg.TopN(ctx, "users", "entity.popularity", LastMonth(), 10)

type BucketSize

type BucketSize string

BucketSize defines the granularity of time-based metric bucketing.

const (
	BucketMinutely BucketSize = "minutely"
	BucketHourly   BucketSize = "hourly"
	BucketDaily    BucketSize = "daily"
	BucketWeekly   BucketSize = "weekly"
	BucketMonthly  BucketSize = "monthly"
)

type BucketValue

type BucketValue struct {
	Bucket string
	Value  float64
}

BucketValue represents a time bucket with its aggregated metric value.

type CRUD

type CRUD[T any, ID comparable] struct {
	// contains filtered or unexported fields
}

CRUD is a decorator that wraps any r3.CRUD[T, ID] and records metrics for every CRUD operation via configurable collectors.

It transparently satisfies the r3.CRUD[T, ID] interface, so it can be used as a drop-in replacement for any CRUD repository.

The metrics store is any r3.CRUD[MetricRecord, string] — the same CRUD abstraction used everywhere in r3. "Everything is a R3po."

Usage:

repo := metrics.WithMetrics[Order, int64](
    innerRepo,
    metricsStore,
    metrics.WithIDFunc[Order, int64](func(o Order) int64 { return o.ID }),
    metrics.WithCollectors[Order, int64](
        metrics.CRUDActionCollector[Order, int64](),
        metrics.PopularityCollector[Order, int64](),
    ),
)

func WithMetrics

func WithMetrics[T any, ID comparable](
	inner r3.CRUD[T, ID],
	store r3.CRUD[MetricRecord, string],
	optFns ...Option[T, ID],
) *CRUD[T, ID]

WithMetrics wraps an existing r3.CRUD with metrics collection.

The store parameter is any r3.CRUD[MetricRecord, string] — use the same CRUD implementation you use for everything else (SQL, GORM, MongoDB, files, etc.).

func (*CRUD[T, ID]) Aggregate added in v0.0.3

func (m *CRUD[T, ID]) Aggregate(ctx context.Context, qarg ...r3.Query) ([]r3.AggregateRow, error)

Aggregate computes grouped aggregates and records metrics (OpAggregate, with TotalCount = number of grouped rows returned). An inner repo without aggregation support yields r3.ErrAggregateNotSupported.

func (*CRUD[T, ID]) Count

func (m *CRUD[T, ID]) Count(ctx context.Context, qarg ...r3.Query) (int64, error)

Count returns the number of matching entities and records metrics.

func (*CRUD[T, ID]) Create

func (m *CRUD[T, ID]) Create(ctx context.Context, entity T) (T, error)

Create inserts a new entity and records metrics.

func (*CRUD[T, ID]) Delete

func (m *CRUD[T, ID]) Delete(ctx context.Context, id ID) error

Delete removes an entity and records metrics.

func (*CRUD[T, ID]) Get

func (m *CRUD[T, ID]) Get(ctx context.Context, id ID, qarg ...r3.Query) (T, error)

Get retrieves an entity by ID and records metrics.

func (*CRUD[T, ID]) Inner

func (m *CRUD[T, ID]) Inner() r3.CRUD[T, ID]

Inner returns the underlying CRUD repository (unwrapped). Useful when you need to bypass metrics for a specific operation.

func (*CRUD[T, ID]) List

func (m *CRUD[T, ID]) List(ctx context.Context, qarg ...r3.Query) ([]T, int64, error)

List retrieves entities and records metrics.

func (*CRUD[T, ID]) Metrics

func (m *CRUD[T, ID]) Metrics() r3.CRUD[MetricRecord, string]

Metrics returns the metric record CRUD for querying metrics directly. Use it with the query builders or the Aggregator to retrieve metric data.

func (*CRUD[T, ID]) Patch

func (m *CRUD[T, ID]) Patch(ctx context.Context, entity T, fields r3.Fields) (T, error)

Patch performs a partial update and records metrics.

func (*CRUD[T, ID]) Rewrap

func (m *CRUD[T, ID]) Rewrap(inner r3.CRUD[T, ID]) r3.CRUD[T, ID]

Rewrap rebuilds this decorator around a different inner CRUD (used to re-apply the metrics layer on top of a transaction-bound CRUD). The metric store and options are shared with the rebuilt instance.

func (*CRUD[T, ID]) Unwrap

func (m *CRUD[T, ID]) Unwrap() r3.CRUD[T, ID]

Unwrap returns the wrapped CRUD so capability detection and transaction propagation can walk the decorator chain.

func (*CRUD[T, ID]) Update

func (m *CRUD[T, ID]) Update(ctx context.Context, entity T) (T, error)

Update modifies an existing entity and records metrics.

type Collector

type Collector[T any, ID comparable] interface {
	Collect(ctx context.Context, opCtx OperationContext[T, ID]) []MetricEntry
}

Collector defines what metrics to emit for CRUD operations. Each collector is called after every CRUD operation and returns zero or more MetricEntry values to record. Returning nil means "don't record anything."

Collectors receive a rich OperationContext with all the information they need. They should NOT perform I/O or side effects — just compute and return entries.

func CRUDActionCollector

func CRUDActionCollector[T any, ID comparable]() Collector[T, ID]

CRUDActionCollector counts every CRUD operation. Emits one "crud.action" entry with value 1 on every successful operation.

The operation type is already included as a core label ("operation") by the decorator, so this collector does not add it.

RecordID is set for entity-level operations (create/get/update/patch/delete) and empty for list operations.

func ErrorCollector

func ErrorCollector[T any, ID comparable]() Collector[T, ID]

ErrorCollector tracks failed CRUD operations. Emits one "crud.error" entry with value 1 when any CRUD operation returns an error.

Unlike other collectors, this fires ONLY on error (Err != nil).

func FieldChangeCollector

func FieldChangeCollector[T any, ID comparable]() Collector[T, ID]

FieldChangeCollector tracks which fields change most often on Update and Patch. Emits one "entity.field_change" entry per changed field. Labels: {"field": "<field_name>"}.

For Patch operations, the fields are taken from the Fields list. For Update operations, this does a shallow reflect-based comparison to detect changes. Requires the old entity to be available (IDFunc must be set).

func LatencyCollector

func LatencyCollector[T any, ID comparable]() Collector[T, ID]

LatencyCollector records the wall-clock duration of each CRUD operation. Emits one "crud.action.latency" entry with value = duration in milliseconds.

Fires on both success and failure — latency is always interesting.

func LifecycleCollector

func LifecycleCollector[T any, ID comparable](createdAtFunc func(T) time.Time) Collector[T, ID]

LifecycleCollector tracks how long entities live (creation to deletion). Emits one "entity.lifecycle" entry on Delete with value = hours since creation.

The createdAtFunc parameter extracts the creation timestamp from the entity. If the entity has no creation time, pass nil and this collector is a no-op.

func ListFilterCollector

func ListFilterCollector[T any, ID comparable]() Collector[T, ID]

ListFilterCollector tracks which fields and operators users use in List queries. Emits one "crud.list.filter" entry per leaf filter in the query. Labels: {"field": "<name>", "operator": "<op>"}.

This helps answer "what do users search for?" -- e.g. 80% of queries filter by "status", so you need an index.

func ListResultSizeCollector

func ListResultSizeCollector[T any, ID comparable]() Collector[T, ID]

ListResultSizeCollector tracks the number of results returned by each List query. Emits one "crud.list.result_size" entry with value = len(results).

Helps identify inefficient queries (too many results) or empty searches.

func ListTotalCountCollector

func ListTotalCountCollector[T any, ID comparable]() Collector[T, ID]

ListTotalCountCollector tracks the total number of matching entities for each List query. Emits one "crud.list.total_count" entry with value = totalCount.

Useful for tracking data volume growth over time.

func PatchSizeCollector

func PatchSizeCollector[T any, ID comparable]() Collector[T, ID]

PatchSizeCollector tracks how many fields are patched at once. Emits one "entity.patch_size" entry on Patch with value = number of fields.

Helps understand usage patterns and API ergonomics.

func PopularityCollector

func PopularityCollector[T any, ID comparable]() Collector[T, ID]

PopularityCollector tracks how often individual entities are accessed. Emits "entity.popularity" with value 1 for each entity accessed:

  • On Get: one entry for the fetched entity, labeled {"source": "get"}
  • On List: one entry per result entity, labeled {"source": "list"}

Requires IDFunc to be set on the decorator options so entity IDs are available. For List operations, entity labelers are applied per-entity (overriding the decorator's single-entity label merge).

type CollectorFunc

type CollectorFunc[T any, ID comparable] func(ctx context.Context, opCtx OperationContext[T, ID]) []MetricEntry

CollectorFunc is a function adapter for Collector, enabling inline collectors:

metrics.CollectorFunc[Order, int64](func(ctx context.Context, opCtx metrics.OperationContext[Order, int64]) []metrics.MetricEntry {
    // custom logic
})

func (CollectorFunc[T, ID]) Collect

func (f CollectorFunc[T, ID]) Collect(ctx context.Context, opCtx OperationContext[T, ID]) []MetricEntry

Collect implements the Collector interface.

type ContextLabeler

type ContextLabeler func(ctx context.Context) Labels

ContextLabeler extracts dimension labels from request context. Useful for labels that come from the HTTP layer, not the entity itself.

Example:

func sourceLabeler(ctx context.Context) metrics.Labels {
    return metrics.Labels{"source": middleware.GetSource(ctx)}
}

type IDFunc

type IDFunc[T any, ID comparable] func(entity T) ID

IDFunc extracts the primary key from an entity and returns it as a comparable value. Used by the decorator to populate RecordID on metric records and to fetch old entity state before updates/deletes.

type Labeler

type Labeler[T any] func(entity T) Labels

Labeler extracts dimension labels from an entity. Multiple labelers can be composed — their outputs are merged into every metric record.

Example:

orderTypeLabeler := func(o Order) metrics.Labels {
    return metrics.Labels{"order_type": o.Type}
}

type Labels

type Labels map[string]string

Labels is a map of key-value string pairs attached to metric records. Labels enable slicing and filtering metrics by arbitrary dimensions (e.g. {"order_type": "manual", "actor_id": "42"}).

func (Labels) Clone

func (l Labels) Clone() Labels

Clone returns a shallow copy of the labels map.

func (Labels) Merge

func (l Labels) Merge(other Labels) Labels

Merge combines two label sets. Values from other take precedence on key conflicts.

type MetricEntry

type MetricEntry struct {
	// MetricName identifies the metric (e.g. "crud.action", "entity.popularity").
	MetricName string

	// Value is the numeric measurement.
	Value float64

	// Labels are collector-specific labels (merged with global/context/core labels by the decorator).
	Labels Labels

	// RecordID is the entity's primary key (stringified). Empty for type-level metrics.
	RecordID string
}

MetricEntry is what a Collector emits before persistence. The decorator enriches it with ID, bucket, timestamps, and merged labels before storing as a MetricRecord.

type MetricRecord

type MetricRecord struct {
	// ID is the unique identifier for this metric record.
	ID string `json:"id" db:"id,pk" bson:"_id"`

	// RecordType is the entity type name (e.g. "users", "orders").
	// Derived automatically from the struct type or set explicitly via options.
	RecordType string `json:"record_type" db:"record_type" bson:"record_type"`

	// RecordID is the primary key of the entity, stringified.
	// Empty for type-level metrics (e.g. list counts).
	RecordID string `json:"record_id" db:"record_id" bson:"record_id"`

	// MetricName identifies the metric (e.g. "crud.action", "entity.popularity").
	MetricName string `json:"metric_name" db:"metric_name" bson:"metric_name"`

	// Value is the numeric measurement (1 for counters, ms for latency, etc.).
	Value float64 `json:"value" db:"value" bson:"value"`

	// Labels carries arbitrary key-value dimensions for slicing metrics.
	// Stored as a JSON blob in SQL databases via JSONColumn.
	Labels r3.JSONColumn[Labels] `json:"labels" db:"labels" bson:"labels"`

	// Bucket is a pre-computed time bucket string for efficient aggregation.
	// Format: ISO8601 truncated to bucket boundary (e.g. "2026-02-16T00:00:00Z" for daily).
	Bucket string `json:"bucket" db:"bucket" bson:"bucket"`

	// CreatedAt is the timestamp when this metric was recorded.
	CreatedAt time.Time `json:"created_at" db:"created_at" bson:"created_at"`
}

MetricRecord represents a single recorded metric data point. It is a first-class r3 entity: it can be stored via any r3.CRUD[MetricRecord, string] implementation (SQL, GORM, MongoDB, etc.).

Everything is a R3po.

type Operation

type Operation string

Operation identifies which CRUD method was invoked.

const (
	OpCreate    Operation = "create"
	OpGet       Operation = "get"
	OpList      Operation = "list"
	OpCount     Operation = "count"
	OpAggregate Operation = "aggregate"
	OpUpdate    Operation = "update"
	OpPatch     Operation = "patch"
	OpDelete    Operation = "delete"
)

type OperationContext

type OperationContext[T any, ID comparable] struct {
	// Operation is the CRUD method that was invoked.
	Operation Operation

	// Duration is the wall-clock time the inner CRUD operation took.
	Duration time.Duration

	// Entity is the result entity (after mutation for create/update/patch,
	// before deletion for delete, fetched entity for get).
	// For list operations, this is the zero value — use Entities instead.
	Entity T

	// Entities is the result set for List operations. Nil for other operations.
	Entities []T

	// OldEntity is the entity state before mutation (for update/patch/delete).
	// Zero value for create/get/list.
	OldEntity T

	// HasOld indicates whether OldEntity is valid.
	HasOld bool

	// ID is the entity primary key (for get/delete). Zero value for create/list.
	EntityID ID

	// TotalCount is the total matching count from List (before pagination). Zero for other ops.
	TotalCount int64

	// Query is the r3.Query passed to Get or List. Empty for mutations.
	Query r3.Query

	// Fields is the field list passed to Patch. Nil for other operations.
	Fields r3.Fields

	// Err is the error returned by the inner CRUD operation (nil on success).
	// Most collectors only fire on success, but ErrorCollector uses this.
	Err error
}

OperationContext carries contextual information about a CRUD operation. It is passed to collectors so they can emit metrics based on what happened.

type Option

type Option[T any, ID comparable] func(*Options[T, ID])

Option is a functional option for configuring the metrics CRUD decorator.

func WithAsync

func WithAsync[T any, ID comparable]() Option[T, ID]

WithAsync enables asynchronous metric recording.

func WithBucketSize

func WithBucketSize[T any, ID comparable](size BucketSize) Option[T, ID]

WithBucketSize sets the time bucketing granularity.

func WithCollectors

func WithCollectors[T any, ID comparable](collectors ...Collector[T, ID]) Option[T, ID]

WithCollectors sets the metric collectors.

func WithContextLabelers

func WithContextLabelers[T any, ID comparable](labelers ...ContextLabeler) Option[T, ID]

WithContextLabelers sets context-based label extractors.

func WithErrorHandler

func WithErrorHandler[T any, ID comparable](fn func(error)) Option[T, ID]

WithErrorHandler sets a custom error handler for metric persistence failures.

func WithIDFunc

func WithIDFunc[T any, ID comparable](fn IDFunc[T, ID]) Option[T, ID]

WithIDFunc sets the function that extracts the primary key from an entity.

func WithLabelers

func WithLabelers[T any, ID comparable](labelers ...Labeler[T]) Option[T, ID]

WithLabelers sets entity-based label extractors.

func WithRecordType

func WithRecordType[T any, ID comparable](name string) Option[T, ID]

WithRecordType sets an explicit record type name for metric records.

type Options

type Options[T any, ID comparable] struct {
	// RecordType is the name used to identify this entity type in metric records.
	// If empty, it is derived automatically from the struct type T
	// (e.g. Order -> "orders", CampaignAdset -> "campaign_adsets").
	RecordType string

	// IDFunc extracts the primary key from an entity.
	// Required for entity-level metrics (popularity, per-entity counters).
	IDFunc IDFunc[T, ID]

	// Collectors defines what metrics to emit for CRUD operations.
	// Multiple collectors are composed — all are called on every operation.
	Collectors []Collector[T, ID]

	// Labelers extract dimension labels from entities.
	// Applied to every metric record where an entity is available.
	Labelers []Labeler[T]

	// ContextLabelers extract dimension labels from request context.
	// Applied to every metric record.
	ContextLabelers []ContextLabeler

	// BucketSize controls the time granularity for metric bucketing.
	// Default: BucketDaily.
	BucketSize BucketSize

	// Async when true records metrics in a background goroutine.
	// The CRUD operation returns immediately; metric recording errors are logged
	// but do not affect the CRUD result. Default: false (synchronous).
	Async bool

	// ErrorHandler is called when metric persistence fails.
	// If nil, errors are logged via slog.
	ErrorHandler func(error)
}

Options configures the behavior of a metrics CRUD decorator.

type QueryOption

type QueryOption func(*queryConfig)

QueryOption configures additional filtering for aggregator methods.

func WithLabel

func WithLabel(key, value string) QueryOption

WithLabel adds a label filter to an aggregator query. Only metric records with this exact label key-value pair will be included.

type RankedEntity

type RankedEntity struct {
	RecordID string
	Value    float64
}

RankedEntity represents an entity ranked by an aggregated metric value.

type RetentionEnforcer

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

RetentionEnforcer runs cleanup of old metric records according to configured policies. It operates on any r3.CRUD[MetricRecord, string] store.

Usage:

enforcer := metrics.NewRetentionEnforcer(metricsStore, metrics.RetentionPolicy{
    MaxAge: 90 * 24 * time.Hour,   // delete records older than 90 days
    MaxRecords: 1_000_000,          // keep at most 1M records per type
})

// Run once (e.g. from a cron job):
enforcer.Enforce(ctx, "orders")

// Run periodically in the background:
stop := enforcer.Start(ctx, "orders", 1*time.Hour)
defer stop()

func NewRetentionEnforcer

func NewRetentionEnforcer(store r3.CRUD[MetricRecord, string], policy RetentionPolicy) *RetentionEnforcer

NewRetentionEnforcer creates a new enforcer with the given policy.

func (*RetentionEnforcer) Enforce

func (e *RetentionEnforcer) Enforce(ctx context.Context, recordType string) int64

Enforce runs a single retention pass for the given record type. It deletes records that violate the policy and returns the number of records deleted. Errors during individual deletions are logged but do not stop the process.

func (*RetentionEnforcer) Start

func (e *RetentionEnforcer) Start(ctx context.Context, recordType string, interval time.Duration) func()

Start runs periodic retention enforcement in a background goroutine. Returns a stop function that cancels the background loop.

type RetentionPolicy

type RetentionPolicy struct {
	// MaxAge is the maximum age of metric records. Records older than this
	// are deleted during enforcement. Zero means no age-based retention.
	MaxAge time.Duration

	// MaxRecords is the maximum number of metric records to keep per entity type.
	// When exceeded, the oldest records are deleted first. Zero means no count-based limit.
	MaxRecords int64
}

RetentionPolicy defines rules for automatic cleanup of old metric records. Multiple policies can be combined — the most aggressive one wins.

type RollupExecutor

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

RollupExecutor compacts fine-grained metric records into coarser summaries.

The rollup process:

  1. Query records matching the source bucket and age criteria.
  2. Group by (record_type, metric_name, target_bucket, label set).
  3. Sum values within each group into a single summary record.
  4. Delete the original fine-grained records.
  5. Insert the summary records.

Usage:

executor := metrics.NewRollupExecutor(metricsStore, metrics.RollupPolicy{
    SourceBucket: metrics.BucketMinutely,
    TargetBucket: metrics.BucketHourly,
    MinAge:       7 * 24 * time.Hour,
})
executor.Execute(ctx, "orders")

func NewRollupExecutor

func NewRollupExecutor(store r3.CRUD[MetricRecord, string], policy RollupPolicy) *RollupExecutor

NewRollupExecutor creates a new rollup executor.

func (*RollupExecutor) Execute

func (rx *RollupExecutor) Execute(ctx context.Context, recordType string) (int64, int64)

Execute runs a single rollup pass for the given record type. Returns the number of records deleted (compacted) and the number of summary records created.

type RollupPolicy

type RollupPolicy struct {
	// SourceBucket is recorded in the summary's _rollup_source label for provenance.
	// It describes the original granularity of the compacted records
	// (e.g. BucketMinutely, BucketHourly).
	SourceBucket BucketSize

	// TargetBucket is the coarser bucket size to roll up into
	// (e.g. BucketHourly, BucketDaily).
	TargetBucket BucketSize

	// MinAge is the minimum age of records before they become eligible for rollup.
	// Only records older than this are compacted. Zero means all records.
	MinAge time.Duration
}

RollupPolicy defines how to compact old fine-grained metric records into coarser summaries. For example, roll up minutely records older than 7 days into hourly summaries, or daily records older than 90 days into monthly.

type TimeRange

type TimeRange struct {
	From time.Time
	To   time.Time
}

TimeRange defines a time window for querying metrics.

func Last24Hours

func Last24Hours() TimeRange

Last24Hours returns a TimeRange covering the last 24 hours.

func Last30Days

func Last30Days() TimeRange

Last30Days returns a TimeRange covering the last 30 days.

func LastDays

func LastDays(n int) TimeRange

LastDays returns a TimeRange covering the last n days.

func LastHours

func LastHours(n int) TimeRange

LastHours returns a TimeRange covering the last n hours.

func LastMinutes

func LastMinutes(n int) TimeRange

LastMinutes returns a TimeRange covering the last n minutes.

func LastMonth

func LastMonth() TimeRange

LastMonth returns a TimeRange covering the last calendar month.

func LastWeek

func LastWeek() TimeRange

LastWeek returns a TimeRange covering the last 7 days.

func LastYear

func LastYear() TimeRange

LastYear returns a TimeRange covering the last 365 days.

Jump to

Keyboard shortcuts

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