Documentation
¶
Index ¶
- Constants
- Variables
- func CreateTable(ctx ql.TxContext, name string) error
- func InitializeGlobal(ctx context.Context, opts Options) error
- func RenderOverview(w io.Writer, title string, headers []string, rows []OverviewRow) error
- func RenderOverviewWithConfig(w io.Writer, cfg OverviewConfig) error
- func RenderPage(ctx context.Context, w io.Writer, groupId GroupId, title string) error
- func RenderPageAt(ctx context.Context, w io.Writer, groupId GroupId, title string, ...) error
- func RenderPageSummary(ctx context.Context, w io.Writer, groupId GroupId, title string, ...) error
- func RenderPageSummaryAt(ctx context.Context, w io.Writer, groupId GroupId, title string, ...) error
- func RenderPageWithConfig(ctx context.Context, w io.Writer, groupId GroupId, title string, ...) error
- func Track(ctx context.Context, item Item, groupId GroupId, groupIds ...GroupId)
- func WithTrigger(ctx context.Context, t Trigger) context.Context
- type Action
- type AthenaConfig
- type AthenaQuery
- type Cleanup
- type EventCreator
- type EventSender
- type EventSending
- type FilterOption
- type GroupId
- type GroupIds
- type Item
- type Option
- type Options
- type OverviewConfig
- type OverviewFilter
- type OverviewModel
- type OverviewRow
- type PageConfig
- type PageModel
- type Record
- type RecordToSend
- type RecordView
- type RequestTraceId
- type Service
- func (h *Service) Cleanup(ctx context.Context, txStarter ql.TxStarter, before time.Time) error
- func (h *Service) Records(ctx ql.TxContext, groupId GroupId) ([]Record, error)
- func (h *Service) RecordsAt(ctx ql.TxContext, groupId GroupId, createdTime time.Time) ([]Record, error)
- func (h *Service) RenderPage(ctx context.Context, w io.Writer, groupId GroupId, title string) error
- func (h *Service) RenderPageAt(ctx context.Context, w io.Writer, groupId GroupId, title string, ...) error
- func (h *Service) RenderPageSummary(ctx context.Context, w io.Writer, groupId GroupId, title string, ...) error
- func (h *Service) RenderPageSummaryAt(ctx context.Context, w io.Writer, groupId GroupId, title string, ...) error
- func (h *Service) RenderPageWithConfig(ctx context.Context, w io.Writer, groupId GroupId, title string, ...) error
- func (h *Service) SendAsync(ctx context.Context)
- func (h *Service) Track(ctx context.Context, item Item, groupId GroupId, groupIds ...GroupId)
- type SummaryItem
- type Trigger
Constants ¶
const PageParam = "page"
PageParam is the query parameter the overview pager pages with.
Variables ¶
var ErrChannelFull = errors.New("channel is full")
var ErrNoTable = errors.New("no trace table configured")
Functions ¶
func CreateTable ¶
CreateTable creates the history table with the given name together with the indexes used by Records and Cleanup, unless they already exist.
func InitializeGlobal ¶ added in v2.7.4
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 RenderOverview ¶ added in v2.7.6
RenderOverview writes a standalone clickable table; each row links to Link.
func RenderOverviewWithConfig ¶ added in v2.9.6
func RenderOverviewWithConfig(w io.Writer, cfg OverviewConfig) error
RenderOverviewWithConfig is RenderOverview with the optional display elements (filter form, scope note).
func RenderPage ¶ added in v2.7.6
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 []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 []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
Track uses the global history singleton. You need to initialize it using InitializeGlobal first.
func WithTrigger ¶ added in v2.8.14
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 struct {
Description string // e.g. "Cancel item Sword-Pack"
ButtonText string // e.g. "Cancel"
Endpoint string // e.g. "/orders/backoffice/v1/orders/123/items/1/cancel"
ConfirmMessage string // optional confirmation prompt; empty = submit immediately
StatusText string // non-empty = show label instead of button
// true renders a link to Endpoint instead of a form, for an action that only
// navigates. Ignores ConfirmMessage.
Link bool
// RequiredRole gates the whole action: "write" or "admin" means that role on
// this service's own audience, "payment-service:admin" names another
// audience. Empty means always shown. A viewer who may not perform the action
// does not see it at all - a disabled button still tells a read-only user
// which endpoint to curl.
RequiredRole string
}
Action is one row in the actions table on the history detail page. When StatusText is non-empty the row renders a static label instead of a button (e.g. "Cancelled"); otherwise a plain HTML form that POSTs to Endpoint.
The page ships no JavaScript: the form is a form, and a ConfirmMessage renders a Bootstrap modal driven by Bootstrap's own JS. Endpoint must therefore be a URL the browser can resolve - a service behind backoffice builds it from the base path backoffice sends with the fragment request, not from its own internal path.
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
}
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
FilterOption is one entry of an OverviewFilter dropdown.
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
type GroupIds ¶ added in v2.8.17
type GroupIds []GroupId
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 OverviewConfig ¶ added in v2.9.6
type OverviewConfig struct {
Title string
Headers []string
Rows []OverviewRow
Filters []OverviewFilter
ScopeNote string
// Page is the 1-based page number shown; 0 and 1 both mean the first page.
// HasNext tells the pager that another page exists - the caller knows this by
// loading one row more than it displays, so no count query is needed.
Page int
HasNext bool
}
OverviewConfig bundles everything the overview page renders. Mirrors PageConfig, so a new display element does not grow the argument list of RenderOverview.
type OverviewFilter ¶ added in v2.9.6
type OverviewFilter struct {
Label string
Name string
Value string
Placeholder string
// Type is the HTML input type; empty means "text". "date" gets the browser's
// own date picker, no JavaScript needed.
Type string
// Options, when non-empty, renders a <select> instead of an input. The empty
// value must be part of the list to allow "no filter".
Options []FilterOption
// Hidden keeps the value in the form and in the pagination links without
// showing a control - for a filter the page receives from elsewhere (a deep
// link carrying a player id) rather than one the viewer types.
Hidden bool
}
OverviewFilter is one field of the overview filter form. Value is the currently applied value, echoed back so the form stays sticky.
func (OverviewFilter) InputType ¶ added in v2.9.8
func (f OverviewFilter) InputType() string
InputType is the type attribute of a text-ish filter input.
type OverviewModel ¶ added in v2.7.6
type OverviewModel struct {
Title string
Headers []string
Rows []OverviewRow
// Filters renders a GET form above the table; empty means no form. Only
// free filters belong here (a player id, a request id). Tenant scope
// (operator, shop) travels as a header set by backoffice, never as a form
// field a viewer can retype.
Filters []OverviewFilter
// ScopeNote is one line naming the scope the rows were loaded with (e.g.
// "operator bmh-audio-pt, all shops"), so the viewer sees why the list is
// short.
ScopeNote string
// Page is the 1-based page currently shown; PrevLink/NextLink are empty when
// there is no such page.
Page int
PrevLink string
NextLink string
}
OverviewModel is the template model for a clickable list page linking to per-item detail pages.
type OverviewRow ¶ added in v2.7.6
OverviewRow is one list entry; Cells aligns with OverviewModel.Headers and Link is the detail-page URL the row navigates to.
type PageConfig ¶ added in v2.8.18
type PageConfig struct {
Summary []SummaryItem
Actions []Action
CreatedAt time.Time // zero = local-only, non-zero = Athena fallback
// 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
}
PageConfig bundles all optional display elements for a history detail page. Use with RenderPageWithConfig to avoid the combinatorial explosion of RenderPage* method variants.
type PageModel ¶ added in v2.7.6
type PageModel struct {
Title string
GroupType string
GroupId string
ErrorMessage string
Summary []SummaryItem
Actions []Action
Records []RecordView
}
PageModel is the template model for the generic history page.
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.
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
// ShowSeparator is true when this record starts a new RequestTraceId group.
ShowSeparator bool
}
RecordView wraps a Record with its pretty-printed JSON payload for rendering.
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.
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
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) Records ¶
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
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 an <hr> separates consecutive RequestTraceId groups.
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 []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 []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 ¶
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 ¶
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 struct {
Label string
Value string
// Link, when non-empty, renders Value as an <a href> to this URL instead of
// plain text - e.g. a cross-service backoffice link to the page that owns the
// referenced entity (a payment or draw history page behind the api-gateway).
Link string
// RequiredRole gates the Link only, in the same notation as
// Action.RequiredRole. A denied item still shows Label and Value, just not as
// an anchor: the value itself is not the secret, the page behind it is.
RequiredRole string
}
SummaryItem is one label/value row shown above the ledger, describing the current state of the tracked object. Ordered slice (not a map) so the page renders stably.
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
Display renders the trigger for the history page, e.g. "message-broker: topic payment_captured (kafkaEventId=evt_1)".
func (Trigger) JSON ¶ added in v2.8.14
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.