history

package
v2.10.29 Latest Latest
Warning

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

Go to latest
Published: Sep 2, 2026 License: MIT Imports: 33 Imported by: 0

Documentation

Index

Constants

View Source
const (
	LevelOk    = "ok"
	LevelWarn  = "warn"
	LevelError = "err"
)

Severity levels a PageConfig.Level classifier may return. Anything else - including the empty string every unclassified record carries - renders a neutral grey pip: informative, no claim about severity.

View Source
const PageParam = boff.PageParam

Variables

View Source
var ErrChannelFull = errors.New("channel is full")
View Source
var ErrNoTable = errors.New("no trace table configured")

Functions

func CreateTable

func CreateTable(ctx ql.TxContext, name string) error

CreateTable creates the history table with the given name together with the indexes used by Records and Cleanup, unless they already exist.

func FlushPending added in v2.10.27

func FlushPending(ctx context.Context)

FlushPending uses the global history singleton to write a record stashed by Pending that no transaction picked up. Pending itself needs no instance: it only puts the record on the context.

func HistoryItemsBlock added in v2.9.10

func HistoryItemsBlock(views []RecordView) boff.Block

HistoryItemsBlock renders the record ledger: one block per request trace, oldest trace first, each block a top-to-bottom timeline of its records - the whole page reads as one timeline, oldest at the top. Every payload starts collapsed behind a chip showing its field count. Always renders (shows a "No history records." note when views is empty).

Use HistoryItemsBlockOrdered to read the ledger in the other direction.

func HistoryItemsBlockCollapsed deprecated added in v2.10.20

func HistoryItemsBlockCollapsed(views []RecordView) boff.Block

HistoryItemsBlockCollapsed is HistoryItemsBlock.

Deprecated: payloads are always collapsed now, so there is nothing left to choose. It stays for callers that named it explicitly.

func HistoryItemsBlockOrdered added in v2.10.28

func HistoryItemsBlockOrdered(views []RecordView, order TraceOrder) boff.Block

HistoryItemsBlockOrdered is HistoryItemsBlock with an explicit reading direction; the zero TraceOrder is the default (everything oldest first).

func InitializeGlobal added in v2.7.4

func InitializeGlobal(ctx context.Context, opts Options) error

InitializeGlobal sets up the global history Service used by Track. It creates the history table (if it does not exist yet) and starts the background task that sends records tracked outside of a transaction.

func Pending added in v2.10.27

func Pending(ctx context.Context, item Item, groupId GroupId, groupIds ...GroupId) context.Context

Pending stashes a record on ctx instead of writing it right away. The first Track that runs in a transaction under the returned context writes it there, first; FlushPending writes it if no transaction did.

This is for the entry that records "we received X" right before the work X triggers: the record belongs in that work's transaction, and the call site (a kafka consumer, a webhook) does not own that transaction. A transaction of its own would cost a connection acquire and a commit on the hot path.

Ordering on the history page is unaffected: timestamp and provenance are taken here, at the Pending call.

func RenderOverview added in v2.7.6

func RenderOverview(w io.Writer, title string, headers []string, rows []OverviewRow) error

RenderOverview forwards to boff.RenderOverview.

func RenderOverviewWithConfig added in v2.9.6

func RenderOverviewWithConfig(w io.Writer, cfg OverviewConfig) error

RenderOverviewWithConfig forwards to boff.RenderOverviewWithConfig.

func RenderPage added in v2.7.6

func RenderPage(ctx context.Context, w io.Writer, groupId GroupId, title string) error

RenderPage uses the global history singleton to render the history page for groupId. You need to initialize it using InitializeGlobal first.

func RenderPageAt added in v2.7.13

func RenderPageAt(ctx context.Context, w io.Writer, groupId GroupId, title string, createdTime time.Time) error

RenderPageAt is RenderPage with the Athena fallback: createdTime decides whether records are read from the local table or from Athena.

func RenderPageSummary added in v2.7.6

func RenderPageSummary(ctx context.Context, w io.Writer, groupId GroupId, title string, summary []boff.SummaryItem) error

RenderPageSummary is RenderPage with a current-state summary above the ledger.

func RenderPageSummaryAt added in v2.7.13

func RenderPageSummaryAt(ctx context.Context, w io.Writer, groupId GroupId, title string, summary []boff.SummaryItem, createdTime time.Time) error

RenderPageSummaryAt is RenderPageSummary with the Athena fallback (see RenderPageAt).

func RenderPageWithConfig added in v2.8.18

func RenderPageWithConfig(ctx context.Context, w io.Writer, groupId GroupId, title string, cfg PageConfig) error

RenderPageWithConfig renders the history page using PageConfig for all optional display elements (summary, actions, Athena fallback).

func Track added in v2.7.4

func Track(ctx context.Context, item Item, groupId GroupId, groupIds ...GroupId)

Track uses the global history singleton. You need to initialize it using InitializeGlobal first.

func WithTrigger added in v2.8.14

func WithTrigger(ctx context.Context, t Trigger) context.Context

WithTrigger tags ctx with the provenance of the work about to be recorded. Set it once at each entry point (HTTP handler, kafka consumer, scheduler); every Track call under that ctx inherits it, so history entries say where they came from without threading the trigger through every layer.

Types

type Action added in v2.8.18

type Action = boff.Action

type AthenaConfig added in v2.7.13

type AthenaConfig struct {
	// required Athena configuration
	Database       string
	Table          string
	WorkGroup      string
	OutputLocation string

	// optional AWS region
	Region string

	// LookupThreshold selects Athena over the local table once the tracked
	// object is older than this. Defaults to 24h when zero.
	LookupThreshold time.Duration

	// LookbackMargin is subtracted from the object creation time to form the
	// Athena query's MinTimestamp, bounding the scanned partitions. Defaults
	// to 30m when zero.
	LookbackMargin time.Duration
}

AthenaConfig enables the Athena read fallback on a Service (see WithAthena).

type AthenaQuery

type AthenaQuery struct {
	GroupId GroupId

	// required athena configuration
	Database       string
	Table          string
	WorkGroup      string
	OutputLocation *url.URL

	// Optional region
	Region string

	// optional values to reduce the amount of data that
	// the query needs to scan. Either value can be left empty to
	MinTimestamp *time.Time
	MaxTimestamp *time.Time
}

func (AthenaQuery) Records

func (q AthenaQuery) Records(ctx context.Context) ([]Record, error)

type Cleanup

type Cleanup struct {
	Interval time.Duration
	Jitter   time.Duration
	MaxAge   time.Duration
}

func (Cleanup) Run

func (c Cleanup) Run(ctx context.Context, service *Service)

Run executes cleanup on the given service in a loop waiting for Cleanup.Interval + rand(Cleanup.Jitter) before each Service.Cleanup. It will delete all items that are older than Cleanup.MaxAge.

type DefaultBlocks added in v2.9.10

type DefaultBlocks struct {
	Header  boff.Block
	Summary boff.Block
	Actions boff.Block
	Records boff.Block
}

DefaultBlocks are the built-in blocks a page renders out of the box, handed to a PageConfig.Blocks callback so custom layouts can reuse them.

func (DefaultBlocks) All added in v2.9.10

func (d DefaultBlocks) All() []boff.Block

All returns the default blocks in their default order.

type EventCreator

type EventCreator func(serviceId, serviceVersion string, rec RecordToSend) events.Event

EventCreator builds the event that is sent out for a tracked record. It receives the ServiceId and ServiceVersion from the EventSending config.

type EventSender

type EventSender = events.EventSender

EventSender is re-exported from the events package for convenience.

type EventSending

type EventSending struct {
	// EventSender sends the events created by EventCreator.
	EventSender EventSender
	// EventCreator builds the event that is sent for a tracked record.
	EventCreator EventCreator

	// ServiceId and ServiceVersion identify the sender of the events. If left
	// empty, they are loaded from the SERVICE_ID and SERVICE_VERSION environment
	// variables in New.
	ServiceId      string
	ServiceVersion string

	// Write events to the kafka outbox first. If false, events are sent async.
	WriteToOutbox bool
}

EventSending groups options required for sending events to an EventSender.

type FilterOption added in v2.9.8

type FilterOption = boff.FilterOption

type GroupId

type GroupId struct {
	// GroupType is the logical type of a tracked object (e.g. "order", "player").
	// It disambiguates a Id so different object types can reuse the same id
	// space without colliding.
	// Must not be empty
	Type string

	// Must not be empty
	Id string
}

GroupId groups multiple history.Record instances into one history trace.

func MakeGroupId added in v2.8.16

func MakeGroupId(typ, id string) GroupId

func (GroupId) LogValue

func (g GroupId) LogValue() slog.Value

func (GroupId) String

func (g GroupId) String() string

type GroupIds added in v2.8.17

type GroupIds []GroupId

func (GroupIds) FirstOf added in v2.8.17

func (ids GroupIds) FirstOf(ty string) GroupId

FirstOf returns the first group id that has a specific Type, or an empty GroupId, if no id was found

func (GroupIds) Strings added in v2.8.17

func (ids GroupIds) Strings() []string

Strings returns all GroupId values mapped to strings

type Item

type Item interface {
	// HistoryString returns a human-readable description of this trace.Item.
	HistoryString() string
}

Item must be implemented by every event that is to be traced. Every Item should be json serializable.

type Option added in v2.7.13

type Option func(*Service)

Option customizes a Service created with New.

func WithAthena added in v2.7.13

func WithAthena(cfg AthenaConfig) Option

WithAthena enables the Athena fallback for reads: RecordsAt loads records from Athena instead of the local table when the tracked object is older than AthenaConfig.LookupThreshold. See RecordsAt.

type Options added in v2.7.4

type Options struct {
	// DB starts the transactions used to create the table and to write records.
	DB ql.TxStarter

	// ServiceId identifies this service as the sender of the emitted events.
	ServiceId string
	// HistoryTable is the name of the table that history records are written to.
	HistoryTable string
	// EventCreator builds the event that is sent out for every tracked record.
	EventCreator EventCreator
	// Athena, when set, enables the Athena read fallback for old records
	// (see WithAthena and Service.RecordsAt).
	Athena *AthenaConfig
}

Options configures the global history Service set up by InitializeGlobal.

type OverviewCell added in v2.9.9

type OverviewCell = boff.OverviewCell

type OverviewConfig added in v2.9.6

type OverviewConfig = boff.OverviewConfig

type OverviewFilter added in v2.9.6

type OverviewFilter = boff.OverviewFilter

type OverviewRow added in v2.7.6

type OverviewRow = boff.OverviewRow

type PageConfig added in v2.8.18

type PageConfig struct {
	Summary   []boff.SummaryItem
	Actions   []boff.Action
	CreatedAt time.Time // zero = local-only, non-zero = Athena fallback

	// CollapseAllPayloads has no effect: every payload is collapsed behind its
	// own chip now.
	//
	// Deprecated: kept so existing callers still compile.
	CollapseAllPayloads bool

	// Level classifies a record's severity for the pip on its ledger row:
	// LevelOk, LevelWarn, LevelError, or "" for neutral. Nil means every record
	// is neutral - this package does not guess a severity from a step name, since
	// only the service writing the ledger knows which of its steps are bad news.
	// A page with no classifier also renders no severity filter.
	Level func(Record) string

	// Order is the reading direction of the ledger. The zero value reads oldest
	// first, both across trace blocks and inside them.
	Order TraceOrder

	// Viewer overrides the identity used for RequiredRole gating. Normally the
	// identity comes from the request context; set this when rendering outside a
	// request (a test, a report). No viewer at all means every gated element is
	// omitted - fail closed.
	Viewer *jwt.Identity

	// Blocks overrides the sections rendered on the page. When nil the page uses
	// its default layout: a HeaderBlock, then SummaryBlock, ActionsBlock and
	// HistoryItemsBlock built from the fields above. When set, the callback
	// receives those default blocks (already gated) plus the loaded record views,
	// and returns the blocks to render in order - so a caller can reorder them,
	// drop one, or splice its own Block in between.
	Blocks func(defaults DefaultBlocks) []boff.Block
}

PageConfig bundles all optional display elements for a history detail page. Use with RenderPageWithConfig to avoid the combinatorial explosion of RenderPage* method variants.

type Record

type Record struct {
	Timestamp      time.Time       `db:"timestamp"`
	RequestTraceId RequestTraceId  `db:"request_trace_id"`
	Step           string          `db:"step"`
	Description    string          `db:"description"`
	Payload        json.RawMessage `db:"payload"`
	Trigger        Trigger         `db:"trigger"`

	// Optional field that might indicate the sender of an event. This is useful if the
	// event comes from an external source, e.g. athena.
	// For local data, it is set to either the serviceId or to `local`, if no serviceId
	// is specified.
	EventSender        string `db:"-"`
	EventSenderVersion string `db:"-"`
}

Record describes one tracing record.

func RecordsAt added in v2.7.13

func RecordsAt(ctx ql.TxContext, groupId GroupId, createdTime time.Time) ([]Record, error)

RecordsAt uses the global history singleton to load records for groupId with the Athena fallback (see Service.RecordsAt).

type RecordToSend

type RecordToSend struct {
	GroupIds       GroupIds
	Timestamp      time.Time
	Step           string
	Description    string
	Payload        json.RawMessage
	RequestTraceId RequestTraceId
	Trigger        Trigger
}

RecordToSend is a single tracked record, ready to be written to the history table and/or converted into an event.

type RecordView added in v2.7.6

type RecordView struct {
	Record
	// JSON is the indented payload.
	JSON string
	// Level is the severity of this record, one of LevelOk, LevelWarn,
	// LevelError, or empty for neutral. It comes from PageConfig.Level; without a
	// classifier every record is neutral.
	Level string
	// Key identifies this record's payload panel on the page. It is derived from
	// the trace and the position inside it - a Record carries no id of its own -
	// and is stable across a refetch, which is what lets an expanded payload
	// survive one.
	Key string
}

RecordView wraps a Record with its pretty-printed JSON payload for rendering.

func (RecordView) ActorId added in v2.10.28

func (v RecordView) ActorId() string

ActorId is who initiated the work, empty for an anonymous or system-triggered record.

func (RecordView) ActorType added in v2.10.28

func (v RecordView) ActorType() string

ActorType is the kind of principal behind ActorId (player, staff, service).

func (RecordView) FieldCount added in v2.10.28

func (v RecordView) FieldCount() int

FieldCount is how many top-level fields the payload carries - what the payload chip shows. Zero means no chip at all.

func (RecordView) HTTPSource added in v2.10.28

func (v RecordView) HTTPSource() string

HTTPSource is the "POST /public/v1/checkout" segment of a record's second line, empty for a record that did not arrive over HTTP (a consumer, a scheduler) - such a row simply omits the segment instead of showing an empty one.

func (RecordView) HasPayload added in v2.10.2

func (v RecordView) HasPayload() bool

HasPayload reports whether the record carries a payload worth displaying - i.e. it is not empty and not just "{}", which many events carry as a placeholder body.

func (RecordView) JSONHTML added in v2.9.8

func (v RecordView) JSONHTML() template.HTML

JSONHTML is the payload colourised with Bootstrap text-colour utilities. Those classes come from the stylesheet the page already has - server-side because a client-side highlighter would be dropped together with the <head> when the page is embedded as a backoffice fragment.

func (RecordView) Millis added in v2.10.28

func (v RecordView) Millis() string

Millis is the fractional part of the record's time, rendered faint next to TimeOfDay so a burst of records within one second stays readable.

func (RecordView) SourceRef added in v2.10.28

func (v RecordView) SourceRef() string

SourceRef is the id of the request or message this record came from, e.g. a request id. Empty when the trigger carries none.

func (RecordView) SourceRefLabel added in v2.10.28

func (v RecordView) SourceRefLabel() string

SourceRefLabel names the kind of SourceRef for the row's title attribute.

func (RecordView) TimeOfDay added in v2.10.28

func (v RecordView) TimeOfDay() string

TimeOfDay is the record's time without its date, which the trace header shows once for the whole block.

type RequestTraceId

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

func (RequestTraceId) IsValid

func (h RequestTraceId) IsValid() bool

func (*RequestTraceId) Scan

func (h *RequestTraceId) Scan(src any) error

func (RequestTraceId) String

func (h RequestTraceId) String() string

func (RequestTraceId) Value

func (h RequestTraceId) Value() (driver.Value, error)

type Service

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

Service traces events by writing them to a history table and/or sending them out as events. Create an instance with New.

func New

func New(txStarter ql.TxStarter, table pgx.Identifier, eventSending *EventSending, opts ...Option) *Service

New creates a new history.Service instance to trace events. By default the service writes records to the history table given by table.

If you specify the optional eventSending parameter, every tracked record is also turned into an event. With EventSending.WriteToOutbox the event is written to the kafka outbox as part of the same transaction (via EventSender.SendInTx); otherwise it is sent asynchronously once the transaction commits (via EventSender.SendAsync).

If Service.SendAsync was called prior, trace events that are not tracked within a transaction are put into a channel (without blocking) and are sent later.

You can specify parameter table as nil to not write to the history table. If you specify an EventSending config, history entries are then only sent out as events.

func (*Service) Cleanup

func (h *Service) Cleanup(ctx context.Context, txStarter ql.TxStarter, before time.Time) error

Cleanup deletes all events that happened before the given timestamp.

func (*Service) FlushPending added in v2.10.27

func (h *Service) FlushPending(ctx context.Context)

FlushPending writes a record stashed by Pending that no transaction picked up, in a transaction of its own. Deferred by the call site that stashed it, so a handler that opened no transaction - or whose transaction rolled back - still leaves its entry.

func (*Service) Records

func (h *Service) Records(ctx ql.TxContext, groupId GroupId) ([]Record, error)

Records returns all local records whose group_ids contain the given groupId. This method does not guarantee any ordering between the records returned.

func (*Service) RecordsAt added in v2.7.13

func (h *Service) RecordsAt(ctx ql.TxContext, groupId GroupId, createdTime time.Time) ([]Record, error)

RecordsAt returns the records for groupId. With Athena configured (WithAthena) it bridges the local table and long-term Athena storage, because a cleanup job deletes local rows after a retention window while Athena keeps them forever:

  • createdTime older than AthenaConfig.LookupThreshold: read from Athena, falling back to the local table if Athena fails.
  • createdTime within the threshold: read the local table only.
  • createdTime zero (age unknown): read the local table first and, only when it returns nothing, fall back to Athena — so records aged out of the local table are still found.

Any Athena failure is logged and never fails the read.

func (*Service) RenderPage added in v2.7.6

func (h *Service) RenderPage(ctx context.Context, w io.Writer, groupId GroupId, title string) error

RenderPage writes a standalone HTML history page for groupId to w. Records are loaded in a new read transaction, sorted by Timestamp (Service.Records is unordered), and each RequestTraceId group is rendered in its own card.

ponytail: payload is rendered as pretty JSON only; add a key/value table when an item needs structured display.

func (*Service) RenderPageAt added in v2.7.13

func (h *Service) RenderPageAt(ctx context.Context, w io.Writer, groupId GroupId, title string, createdTime time.Time) error

RenderPageAt is RenderPage with an Athena fallback: records are loaded via RecordsAt using createdTime to decide between the local table and Athena.

func (*Service) RenderPageSummary added in v2.7.6

func (h *Service) RenderPageSummary(ctx context.Context, w io.Writer, groupId GroupId, title string, summary []boff.SummaryItem) error

RenderPageSummary is RenderPage with an extra current-state summary rendered above the ledger.

func (*Service) RenderPageSummaryAt added in v2.7.13

func (h *Service) RenderPageSummaryAt(ctx context.Context, w io.Writer, groupId GroupId, title string, summary []boff.SummaryItem, createdTime time.Time) error

RenderPageSummaryAt is RenderPageSummary with the Athena fallback (see RenderPageAt).

func (*Service) RenderPageWithConfig added in v2.8.18

func (h *Service) RenderPageWithConfig(ctx context.Context, w io.Writer, groupId GroupId, title string, cfg PageConfig) error

RenderPageWithConfig renders the history page using PageConfig for all optional display elements (summary, actions, Athena fallback).

func (*Service) SendAsync

func (h *Service) SendAsync(ctx context.Context)

SendAsync starts the async process. This will start a background task to send out records that are not traced within a transaction. You can stop the background task by canceling the Context ctx.

func (*Service) Track

func (h *Service) Track(ctx context.Context, item Item, groupId GroupId, groupIds ...GroupId)

Track records the given item under groupId. Depending on the Service configuration the record is written to the history table and/or sent out as an event. Tracking never returns an error: any failure is only logged.

If the context carries a transaction, the record is written within it. Otherwise a new transaction is opened, unless async sending was enabled via Service.SendAsync, in which case the record is queued without blocking.

type SummaryItem added in v2.7.6

type SummaryItem = boff.SummaryItem

type TraceOrder added in v2.10.28

type TraceOrder struct {
	// NewestTracesFirst puts the most recent trace block at the top.
	NewestTracesFirst bool

	// NewestEventsFirst reverses the records inside every trace block.
	NewestEventsFirst bool
}

TraceOrder is the reading direction of the ledger. Its zero value is the default: everything oldest first, so the page reads as one timeline from top to bottom - the tracked object is created at the top and reaches its current state at the bottom.

The two axes are independent on purpose. "Newest trace at the top, but each trace still read forwards" is a real way to work a busy ledger: the most recent request is what an operator came for, and inside it they still want cause before effect.

type TraceView added in v2.10.28

type TraceView struct {
	// Id is the request trace id shared by every record in the block.
	Id string
	// Events are the records of this trace, oldest first - a trace block reads
	// top to bottom as the request unfolded.
	Events []RecordView
}

TraceView is one request trace: every record written while handling the same incoming request, in the order they happened. The ledger renders one block per trace, so a page reads as "what did this request do" rather than as one long undifferentiated list.

func (TraceView) Count added in v2.10.28

func (t TraceView) Count() int

Count is how many records the trace holds.

func (TraceView) Date added in v2.10.28

func (t TraceView) Date() string

Date is the day the trace happened, shown once in the trace header so the rows themselves can show a time only.

func (TraceView) Duration added in v2.10.28

func (t TraceView) Duration() string

Duration is how long the trace took, from its first record to its last - independent of the direction the block is rendered in.

func (TraceView) End added in v2.10.28

func (t TraceView) End() time.Time

End is when the trace's last record was written.

func (TraceView) Start added in v2.10.28

func (t TraceView) Start() time.Time

Start is when the trace's first record was written, whichever end of the block that record now sits at.

type Trigger added in v2.8.14

type Trigger struct {
	Source  string `json:"source"`            // e.g. http, message-broker, scheduler
	Detail  string `json:"detail,omitempty"`  // e.g. "POST /checkout", "topic payment_captured"
	RefType string `json:"refType,omitempty"` // kind of the source id, e.g. requestId, kafkaEventId
	Ref     string `json:"ref,omitempty"`     // the source id value (request/event this entry came from)

	// Actor is who initiated the work, filled from the context by triggerOf when
	// the caller did not set it. No migration: trigger is a JSON column and the
	// event field is a JSON string, so an older reader just ignores the key.
	Actor actor.Actor `json:"actor,omitzero"`
}

Trigger records what caused a history entry: the transport it arrived on and, where known, who or what initiated it. It is sourced from context (WithTrigger) by Track, stored in the history table's trigger column, and carried on the emitted event (via RecordToSend) so it survives to long-term Athena storage.

func (Trigger) Display added in v2.8.14

func (t Trigger) Display() string

Display renders the trigger for the history page, e.g. "message-broker: topic payment_captured (kafkaEventId=evt_1)".

func (Trigger) IsZero added in v2.8.14

func (t Trigger) IsZero() bool

IsZero reports whether no provenance was set.

func (Trigger) JSON added in v2.8.14

func (t Trigger) JSON() string

JSON returns the encoding used on the wire (event trigger field) and in the trigger column, or "" when no provenance is set. EventCreators map it onto their event's trigger field.

func (*Trigger) Scan added in v2.8.14

func (t *Trigger) Scan(src any) error

Scan implements sql.Scanner, decoding the JSON trigger column (NULL or empty yields the zero Trigger).

func (Trigger) Value added in v2.8.14

func (t Trigger) Value() (driver.Value, error)

Value implements driver.Valuer, storing the trigger as JSON or NULL when empty.

Jump to

Keyboard shortcuts

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