usagelog

package
v0.4.2 Latest Latest
Warning

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

Go to latest
Published: Jun 28, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Package usagelog is the first consumer of pkg/lifecycle's Hook + Collector surface. Per-request usage records — pure attribution + token counts.

Four pieces, split along produce → attach → store:

  • Event: the canonical usage schema. Stable JSON shape — sinks (file JSONL today, ClickHouse later) consume by deserializing this struct directly. No cost, no pricing — those are downstream consumer concerns. The event answers "who called what model on which host and how many tokens." Cost layers on at query time.
  • UsageHook: the lifecycle.Hook (producer). Parses tokens + finish reason out of the response body via v1.ExtractSummary, builds the Event, returns it. Pure — the Registry attaches it to the Context under Namespace; the hook never touches the Context.
  • SinkCollector: the lifecycle.Collector (janitor / store). Reads the attached Event back off the Context and pushes it onto the Emitter.
  • Emitter: bounded-queue dispatcher. One drain goroutine writes to each Sink. Drop-on-full with counter — never blocks the post-flight goroutine.

The split is what lets a pre-send reader (usage echo) and the sink share one collection: the hook produces once, the Registry attaches, and any number of readers/collectors consume it.

Index

Constants

View Source
const (
	DefaultEventLimit = usage.DefaultEventLimit
	MaxEventLimit     = usage.MaxEventLimit
	MaxBuckets        = usage.MaxBuckets
)
View Source
const DefaultQueueSize = 1024

DefaultQueueSize is the default bounded-channel capacity if EmitterOptions.QueueSize is zero. 1024 events comfortably absorbs a brief drain stall (sink slow / disk flush / pipe back-pressure) without blocking the post-flight goroutine.

View Source
const MaxTagsHeaderBytes = 2048

MaxTagsHeaderBytes caps the raw header at the inference edge — longer values are never copied into Metadata.

View Source
const MetadataKeyRequestTags = "request_tags_raw"

MetadataKeyRequestTags is the lifecycle Metadata key under which the inference edge stashes the raw X-WR-Request-Tags header value. The hot path copies the string only; ParseTags runs post-flight.

View Source
const Namespace = "usage"

Namespace is the key under which UsageHook attaches its Event to the lifecycle Context. SinkCollector (store side) and any pre-send reader (usage echo) read it back via lc.Collected(Namespace).

Variables

View Source
var ValidGroupBy = usage.ValidGroupBy

ValidGroupBy lists the accepted GroupBy values (see pkg/usage).

Functions

func EchoFromContext

func EchoFromContext(lc *lifecycle.Context) *v1.RelayUsage

EchoFromContext maps the usage Event that UsageHook attached to lc into the caller-facing v1.RelayUsage block. Returns nil when no usage Event was collected (so the response-writer simply doesn't inject anything).

This is the read side of the blackboard: the producer (UsageHook) filled the Event once; echo reads that same collection here, the sink reads it at Finalize — no re-parse. Deliberately drops operator-only attribution (relay-key hash, policy/host/model UUIDs); the caller gets observability, not internals.

func IsValidGroupBy

func IsValidGroupBy(g string) bool

IsValidGroupBy reports whether g is a supported group dimension.

func ParseTags added in v0.3.0

func ParseTags(raw string) (map[string]string, bool)

ParseTags validates a raw X-WR-Request-Tags value: a flat JSON object with string values only, ≤16 keys, key ≤64 chars, value ≤256 chars. Any violation drops the whole blob (nil, false) — caller tags are best-effort observability and must never affect the request.

Types

type Backend

type Backend struct {
	Sink   Sink
	Reader Reader
}

Backend bundles the sink (write) and reader (read) a log backend provides. For clickhouse/postgres/valkey, Sink and Reader are the same instance; for file they differ. The composition root names the backend packages and builds this; everything else consumes it through the Controller.

type BackendBuilder

type BackendBuilder func(ctx context.Context, cfg settings.UsageLogging) (Backend, error)

BackendBuilder constructs the Backend for a resolved config. Injected by the composition root so backend packages stay named only there.

type Closer

type Closer = usage.Closer

Closer is the optional graceful-shutdown contract a Sink may implement; the Emitter calls it after draining. See pkg/usage.Closer.

type Controller

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

Controller owns the log backend's runtime lifecycle: a long-lived Emitter draining into a reloadable sink, plus a reloadable reader the control plane queries. It reconciles the backend (file ↔ clickhouse ↔ postgres ↔ valkey) on settings changes without a restart — a reroute is a clean break (new events go to the new store; old data stays put, no migration).

Unlike payload-logging there is no enable/maxbytes gate: logging is constant. The swap closes the old sink after in-flight writes drain (reloadableSink's RWMutex); the old reader (== old sink for ch/pg/valkey, or a resource-less file reader) needs no separate close.

func NewController

func NewController(src SettingsSource, build BackendBuilder, log *slog.Logger) *Controller

NewController wires the emitter → reloadable-sink chain and the reloadable reader. The sink/reader start empty until the first reconcile.

func (*Controller) Close

func (c *Controller) Close()

Close drains the emitter (which closes the live sink).

func (*Controller) Emitter

func (c *Controller) Emitter() *Emitter

Emitter returns the long-lived emitter (the collector registers on it).

func (*Controller) Reader

func (c *Controller) Reader() Reader

Reader returns the stable reader handle the control plane queries; it delegates to whichever backend reader is currently live.

func (*Controller) Run

func (c *Controller) Run(ctx context.Context)

Run does the initial reconcile then reconciles on each kick until ctx ends.

func (*Controller) Subscribe

func (c *Controller) Subscribe()

Subscribe registers the settings-change callback (cheap, non-blocking — signals the kick channel; the rebuild runs on Run's goroutine).

type DurationStats

type DurationStats = usage.DurationStats

Query / result types — re-exported from pkg/usage so callers that only import usagelog keep compiling. New code may use pkg/usage directly.

type Emitter

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

Emitter is the fan-out point: hooks Emit() events; a single background goroutine drains the queue and writes to each Sink.

Drop-on-full preserves the "post-flight never blocks" invariant. Drop counter is exposed via Dropped() for /metrics or assertions.

func NewEmitter

func NewEmitter(opts EmitterOptions, sinks ...Sink) *Emitter

NewEmitter constructs an Emitter and starts its drain goroutine. The Emitter must be Closed at shutdown to flush in-flight events.

func (*Emitter) Close

func (e *Emitter) Close()

Close signals the drain goroutine to finish, drains pending events, and returns once all sinks have processed everything in flight. After the queue drains, any sink implementing Closer is closed (flush final batch, close remote conn) so buffered/remote backends lose nothing on graceful shutdown. Subsequent Emit calls are no-ops.

func (*Emitter) Dropped

func (e *Emitter) Dropped() uint64

Dropped returns the cumulative count of events dropped due to a full queue. Useful for /metrics scraping.

func (*Emitter) Emit

func (e *Emitter) Emit(ev Event)

Emit queues ev for delivery to all sinks. Non-blocking; if the queue is full the event is dropped and the drop counter increments. Safe for concurrent calls — the underlying channel handles fan-in.

func (*Emitter) QueueDepth added in v0.3.0

func (e *Emitter) QueueDepth() int

QueueDepth returns the number of events waiting in the bounded queue — the leading signal before Dropped starts counting. Safe to call concurrently (chan len).

type EmitterOptions

type EmitterOptions struct {
	// QueueSize is the bounded-channel capacity. <= 0 → DefaultQueueSize.
	QueueSize int

	// Logger is used for drop / sink-error warnings. nil → slog.Default.
	Logger *slog.Logger
}

EmitterOptions tunes Emitter behavior. Zero values are sensible defaults for typical deployments.

type Event

type Event = usage.Event

Event is the canonical per-request usage record. The canonical type lives in pkg/usage so every backend (file, ClickHouse, valkey, postgres) can consume it without importing app/ (pkg-purity rule). This alias keeps the lifecycle observers in this package ergonomic.

type EventQuery

type EventQuery = usage.EventQuery

Query / result types — re-exported from pkg/usage so callers that only import usagelog keep compiling. New code may use pkg/usage directly.

type Pricer added in v0.3.0

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

Pricer maps a request's stamped pricing identity + extracted token counts to the cost fields stamped on the usage Event. Cost is computed exactly once, here at emit time, from the rate sheet currently in the catalog snapshot — what a request cost is a historical fact, so readers never recompute it against (mutable) pricing config. Nil-safe: a nil Pricer (tests, partial wiring) prices nothing, leaving events unpriced.

func NewPricer added in v0.3.0

func NewPricer(lookup PricingLookup) *Pricer

NewPricer constructs a Pricer over the given lookup.

func (*Pricer) Price added in v0.3.0

func (p *Pricer) Price(pricingID string, tokens sdkusage.Tokens) (nanos int64, breakdown map[string]int64, ok bool)

Price returns the total nano-USD cost + per-meter breakdown for the tokens under pricingID. ok=false means unpriced — no pricing id stamped, the rate sheet is gone from the snapshot, or no token key matched a rate. Unpriced is never reported as a zero cost.

type PricingLookup added in v0.3.0

type PricingLookup func(id string) (*pricing.Pricing, bool)

PricingLookup resolves a pricing id to the rate sheet. Satisfied by a closure over app/catalog's live snapshot at the composition root — snapshot map read, zero I/O.

type Reader

type Reader = usage.Reader

Reader is the read-side interface backends implement. Canonical definition lives in pkg/usage; re-exported here for the control-plane consumers (and cmd/relay-stats) that import usagelog. The concrete backends live under pkg/usage/<backend> (file today, clickhouse/valkey next).

type ReasoningTiming

type ReasoningTiming = sdkusage.ReasoningTiming

ReasoningTiming is the reasoning span — see pkg/usage.ReasoningTiming.

type SettingsSource

type SettingsSource interface {
	Setting(section string) (any, bool)
	OnSettingsChange(section string, fn func())
}

SettingsSource reads a live settings section + subscribes to changes. Satisfied by *app/catalog.Catalog.

type Sink

type Sink = usage.Sink

Sink consumes usage events. Canonical interface in pkg/usage; the concrete backends (file, clickhouse, valkey, postgres) live under pkg/usage/<backend> and implement it. Re-exported here for the Emitter and the composition root.

type SinkCollector

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

SinkCollector is the usage janitor: a lifecycle.Collector that reads the *Event UsageHook attached under Namespace and pushes it onto the bounded Emitter (→ Sink). Read-only on the Context; the Emit is a non-blocking channel send, so it never blocks the post-flight goroutine.

This is the "store" half of the produce → attach → store flow. Adding a second destination (ClickHouse, OTel) is a matter of the Emitter fanning to more Sinks — not a change to this collector or the hook.

func NewSinkCollector

func NewSinkCollector(e *Emitter) *SinkCollector

NewSinkCollector constructs a collector that emits collected usage Events onto e.

func (*SinkCollector) Collect

func (c *SinkCollector) Collect(lc *lifecycle.Context)

Collect reads the usage Event off lc and emits it. No-op when no usage Event was attached (defensive — UsageHook always attaches one).

type StreamUsageFactory

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

StreamUsageFactory is the streaming counterpart to UsageHook: a lifecycle.StreamObserverFactory whose observer buffers the raw upstream SSE frames and, at end-of-stream, builds the same usage Event via the shared buildEvent (which runs v1.ExtractSummary on the accumulated body). Streamed requests therefore collect usage exactly like buffered ones — same Event, same Namespace — so echo and the sink both see one shape. The post-flight Hook is too late for a streamed body (it runs after the stream is already written); this fills during the stream.

func NewStreamUsageFactory

func NewStreamUsageFactory(pricer *Pricer) *StreamUsageFactory

NewStreamUsageFactory constructs the factory. pricer may be nil (events stay unpriced).

func (*StreamUsageFactory) Name

func (*StreamUsageFactory) Name() string

func (*StreamUsageFactory) NewObserver

type SummaryQuery

type SummaryQuery = usage.SummaryQuery

Query / result types — re-exported from pkg/usage so callers that only import usagelog keep compiling. New code may use pkg/usage directly.

type SummaryResult

type SummaryResult = usage.SummaryResult

Query / result types — re-exported from pkg/usage so callers that only import usagelog keep compiling. New code may use pkg/usage directly.

type SummaryRow

type SummaryRow = usage.SummaryRow

Query / result types — re-exported from pkg/usage so callers that only import usagelog keep compiling. New code may use pkg/usage directly.

type TimeSeriesPoint

type TimeSeriesPoint = usage.TimeSeriesPoint

Query / result types — re-exported from pkg/usage so callers that only import usagelog keep compiling. New code may use pkg/usage directly.

type TimeSeriesQuery

type TimeSeriesQuery = usage.TimeSeriesQuery

Query / result types — re-exported from pkg/usage so callers that only import usagelog keep compiling. New code may use pkg/usage directly.

type TimeSeriesResult

type TimeSeriesResult = usage.TimeSeriesResult

Query / result types — re-exported from pkg/usage so callers that only import usagelog keep compiling. New code may use pkg/usage directly.

type TimeSeriesRow

type TimeSeriesRow = usage.TimeSeriesRow

Query / result types — re-exported from pkg/usage so callers that only import usagelog keep compiling. New code may use pkg/usage directly.

type UpstreamTiming

type UpstreamTiming = sdkusage.UpstreamTiming

UpstreamTiming is the upstream-leg timing breakdown — see pkg/usage.UpstreamTiming.

type UsageHook

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

UsageHook is the usage producer: a lifecycle.Hook that builds the canonical per-request Event from the Context's identity/timing and the PostFlightEvent's outcome. Pure — it never touches the Context; the Registry attaches the returned *Event under Namespace. Storing happens later, in SinkCollector.

Canonical-first: token counts + finish reason come from v1.ExtractSummary on the buffered body using the per-request Translator — no vendor-specific JSON/SSE branching at this layer.

func NewUsageHook

func NewUsageHook(pricer *Pricer) *UsageHook

NewUsageHook constructs the usage producer. pricer may be nil (events stay unpriced).

func (*UsageHook) Fill

Fill builds the Event. Always returns one (error rows are valid usage records), so every finalized request yields exactly one row.

func (*UsageHook) Name

func (*UsageHook) Name() string

Jump to

Keyboard shortcuts

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