Help us shape the future of Go! Take the 2026 Go Developer Survey

flowstore

package
v0.5.1 Latest Latest
Warning

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

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

Documentation

Overview

Package flowstore defines the FlowStore interface and its Postgres implementation. All flow reads and writes go through the interface; no other package issues SQL against flow tables. The schema is column-shaped and time-partitioned so a columnar backend can be added behind the same interface when a real estate's scale demands it (ADR-0009).

Package flowstore defines the FlowStore interface and its Postgres implementation. All flow reads and writes go through the interface; no other package issues SQL against flow tables. The schema is column-shaped so a columnar backend can be added behind the same interface when a real estate's scale demands it (ADR-0009, ADR-0019).

Index

Constants

View Source
const (
	DefaultGroupLimit = 200
	MaxGroupLimit     = 1000
)

DefaultGroupLimit is the number of groups a grouped rollup returns when the query sets none; MaxGroupLimit is the most it ever returns. A rollup with more groups than its limit returns the top groups in its order and says so; the screens show top-N with totals, never an unbounded list (ADR-0007 as amended).

View Source
const (
	DefaultPageLimit = 100
	MaxPageLimit     = 500
)

DefaultPageLimit is the number of windows a page holds when the query sets none; MaxPageLimit is the most a page ever holds.

View Source
const DefaultRetention = 30 * 24 * time.Hour

DefaultRetention is how long windows are kept unless configured otherwise. Totals are never pruned.

View Source
const DefaultRetentionInterval = time.Hour

DefaultRetentionInterval is how often a replica attempts a pruning run.

Variables

View Source
var ErrUnknownEndpoint = errors.New("flowstore: unknown endpoint")

ErrUnknownEndpoint is returned for an endpoint other than dst or either.

View Source
var ErrUnknownGroupBy = errors.New("flowstore: unknown grouping")

ErrUnknownGroupBy is returned for a grouping outside GroupBys.

GroupBys lists every grouping, in the order a document names them.

Functions

func GroupByNames

func GroupByNames() string

GroupByNames joins the allowed groupings for a message.

Types

type Endpoint added in v0.2.0

type Endpoint string

Endpoint says which end of a stored record a grouped rollup's workload set selects. A record is reported by its destination workload and names its source as the peer ingestion resolved (ADR-0019 decision 2), so a workload set can select either end without resolving anything at read time but the set itself.

const (
	// EndpointDst selects the records the workloads in the set reported:
	// the traffic that reached them. The default.
	EndpointDst Endpoint = "dst"
	// EndpointEither also selects the records whose source resolved to a
	// workload in the set: the traffic they sent as well as received. A
	// record with both ends in the set is selected once.
	EndpointEither Endpoint = "either"
)

Endpoints of a grouped rollup's workload set.

func ParseEndpoint added in v0.2.0

func ParseEndpoint(s string) (Endpoint, error)

ParseEndpoint resolves an endpoint name; empty is the default, dst.

type FlowStore

type FlowStore interface {
	// WriteWindow stores every record of w, folds each into the totals,
	// and stores its gaps, atomically; a gap already stored is not stored
	// again. It returns the number of records stored.
	WriteWindow(ctx context.Context, w Window) (int, error)
	// WriteGaps stores gaps the control plane derived itself for one
	// workload, outside any reported window; a gap already stored is not
	// stored again. The agent-reported gaps ride WriteWindow.
	WriteGaps(ctx context.Context, id identity.WorkloadID, gaps []Gap) error
	// ListGaps returns the gaps of a workload set intersecting a range.
	ListGaps(ctx context.Context, q GapQuery) ([]GapRow, error)
	// ListWindows returns a workload's stored windows, newest first.
	ListWindows(ctx context.Context, q WindowQuery) ([]WindowRow, error)
	// Rollup groups the windows of a workload set by peer and service.
	Rollup(ctx context.Context, q RollupQuery) ([]RollupRow, error)
	// RollupGroups aggregates windows under one of the named groupings,
	// ordered and bounded as the query says. It returns ErrUnknownGroupBy
	// for a grouping outside GroupBys.
	RollupGroups(ctx context.Context, q GroupQuery) (*GroupResult, error)
	// ListWindowPage returns one page of a workload's windows, newest
	// first, continuing after the query's cursor.
	ListWindowPage(ctx context.Context, q WindowPageQuery) ([]WindowRow, error)
	// ListTotals returns a workload's cumulative flow keys, most recently
	// seen first. A zero decision means every decision.
	ListTotals(ctx context.Context, id identity.WorkloadID, decision innerwallv1.PolicyDecision) ([]Total, error)
	// PruneWindows deletes windows that started before horizon, and gaps
	// that ended before it, in bounded batches. It reports how many window
	// rows it deleted and whether it ran at all: only one replica prunes at
	// a time, and a replica that finds the retention lock held by another
	// returns ran == false and deleted == 0.
	PruneWindows(ctx context.Context, horizon time.Time) (deleted int64, ran bool, err error)
}

FlowStore is the only path to flow data (ADR-0009). Every method is implemented with hand-written SQL in internal/store/queries (ADR-0006).

type Gap

type Gap struct {
	Kind   innerwallv1.EvidenceGapKind
	Source innerwallv1.EvidenceSource
	From   time.Time
	To     time.Time
	Count  *uint64
}

Gap is one interval [From, To) in which a workload's flow evidence is known to be incomplete: as its agent reported it, or, for the one server-derived kind (agent silence), as flow ingest derived it. Count is the records or table entries lost, nil when unknown (ADR-0019 as amended).

type GapQuery

type GapQuery struct {
	WorkloadIDs []identity.WorkloadID
	Since       time.Time
	Until       time.Time
	Limit       int
}

GapQuery selects the gaps of a set of workloads that intersect [Since, Until), newest first, at most Limit of them. An empty set means every workload.

type GapRow

type GapRow struct {
	ID         int64
	WorkloadID identity.WorkloadID
	ReceivedAt time.Time
	Gap
}

GapRow is one stored flow_gaps row.

type GapWriter added in v0.5.0

type GapWriter interface {
	WriteGaps(ctx context.Context, id identity.WorkloadID, gaps []Gap) error
}

GapWriter stores the gaps the control plane derived itself for one workload, as FlowStore.WriteGaps does; a gap already stored is not stored again. The Postgres store hands one out bound to a caller's transaction (Postgres.Within).

type Group

type Group struct {
	RuleID     string
	Peer       Peer
	WorkloadID identity.WorkloadID
	DstPort    uint16
	Protocol   innerwallv1.Protocol

	// WorkloadCount is the number of distinct workloads that saw the
	// group. Only rule and peer,service count it; it is zero for the
	// others: src,dst and dst,service name their one workload in their
	// keys, and rule,peer, whose key names none, is not counted.
	WorkloadCount int64
	// FlowCount is the number of stored records in the group.
	FlowCount       int64
	ConnectionCount uint64
	ByteCount       uint64
	FirstSeen       time.Time
	LastSeen        time.Time
}

Group is one group of a grouped rollup. Which key fields are set depends on the grouping: RuleID for rule and rule,peer; Peer for rule,peer, src,dst, and peer,service; WorkloadID for src,dst and dst,service; DstPort and Protocol for dst,service and peer,service. The peer's labels are the snapshot stored with its most recently seen record in the group (ADR-0019).

type GroupBy

type GroupBy string

GroupBy names one grouping of the windows the store serves. The set is closed: each member maps to one named statement in the store, chosen by the caller, and there is no free-form grouping (ADR-0007 as amended, ADR-0019). The names are the ones the operator surface accepts.

const (
	// GroupByRule groups by the resolved rule that admitted the traffic;
	// records with no matched rule form one group with the empty rule id.
	// Rule hit counters, with the distinct workloads that reported each.
	GroupByRule GroupBy = "rule"
	// GroupByRulePeer groups by rule and the resolved peer that hit it.
	GroupByRulePeer GroupBy = "rule,peer"
	// GroupBySrcDst groups by resolved peer and reporting workload: the
	// edges of the dependency map, and the rows of a would-block review.
	GroupBySrcDst GroupBy = "src,dst"
	// GroupByDstService groups by reporting workload and the service
	// (destination port and protocol) reached on it: the matrix cells.
	GroupByDstService GroupBy = "dst,service"
	// GroupByPeerService groups by resolved peer and the service it
	// reached across every workload in scope, counting the distinct
	// workloads that saw it: the rows of a simulation review (ADR-0019
	// decision 4(b)).
	GroupByPeerService GroupBy = "peer,service"
)

The groupings the store serves. Inbound only in this version (ADR-0010), so the source of a flow is its resolved peer and the destination is the reporting workload; "peer" and "src" name that same resolved-peer dimension, each under the name the screen that uses it speaks.

func ParseGroupBy

func ParseGroupBy(s string) (GroupBy, error)

ParseGroupBy resolves a comma-joined grouping, tolerating spaces around the names. It accepts the members of GroupBys and nothing else.

func (GroupBy) Keys

func (g GroupBy) Keys() []string

Keys returns the dimensions of the grouping, in order.

type GroupOrder

type GroupOrder string

GroupOrder says how a grouped rollup is ordered before it is bounded.

const (
	// OrderByConnections puts the busiest groups first. The default.
	OrderByConnections GroupOrder = "connections"
	// OrderByRecency puts the most recently seen groups first.
	OrderByRecency GroupOrder = "recent"
)

Orders of a grouped rollup.

type GroupQuery

type GroupQuery struct {
	GroupBy     GroupBy
	WorkloadIDs []identity.WorkloadID
	Endpoint    Endpoint
	Since       time.Time
	Until       time.Time
	Decision    innerwallv1.PolicyDecision
	Direction   innerwallv1.Direction
	Protocol    innerwallv1.Protocol
	DstPort     uint16
	Order       GroupOrder
	// Limit bounds the groups returned; DefaultGroupLimit when zero,
	// never more than MaxGroupLimit.
	Limit int
}

GroupQuery is a grouped rollup over the windows in [Since, Until). An empty WorkloadIDs means every workload; Endpoint says which end of a record a non-empty WorkloadIDs selects, the destination when zero; a zero Decision or Direction means any; a zero Protocol means every service, otherwise exactly the service (Protocol, DstPort).

type GroupResult

type GroupResult struct {
	Groups          []Group
	EffectiveFrom   time.Time
	EffectiveTo     time.Time
	GroupCount      int64
	FlowCount       int64
	ConnectionCount uint64
	ByteCount       uint64
}

GroupResult is a grouped rollup. EffectiveFrom and EffectiveTo are the bounds of the windows actually covered, which quantize the requested range to window boundaries; both are zero when no window matched. GroupCount and the totals cover every group, including those beyond the limit.

func (*GroupResult) Truncated

func (r *GroupResult) Truncated() bool

Truncated reports whether groups beyond those returned exist.

type Peer

type Peer struct {
	Kind   PeerKind
	Key    string
	Labels map[string]string
}

Peer is the resolved remote end of a flow as captured at ingest. Labels is the workload's label set at that moment and is empty for the other kinds; it is stored with the row because labels and addresses change and a query-time join would rewrite what the operator saw (ADR-0019).

type PeerKind

type PeerKind int32

PeerKind says what a flow record's source address resolved to at ingest.

const (
	// PeerUnknown is an address that matched no workload and no address
	// group; the peer key is the address itself.
	PeerUnknown PeerKind = 0
	// PeerWorkload is a managed workload; the peer key is its id.
	PeerWorkload PeerKind = 1
	// PeerAddressGroup is a named address group; the peer key is its id.
	PeerAddressGroup PeerKind = 2
)

Peer kinds, in the order the schema encodes them.

func (PeerKind) String

func (k PeerKind) String() string

String names the kind for display.

type Postgres

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

Postgres is the FlowStore over the control plane's database.

func NewPostgres

func NewPostgres(pool *pgxpool.Pool) *Postgres

NewPostgres wraps an open pool.

func (*Postgres) CountWindows

func (p *Postgres) CountWindows(ctx context.Context) (int64, error)

CountWindows returns the number of stored windows. Exposed for tests and diagnostics.

func (*Postgres) ListGaps

func (p *Postgres) ListGaps(ctx context.Context, q GapQuery) ([]GapRow, error)

ListGaps implements FlowStore.

func (*Postgres) ListTotals

func (p *Postgres) ListTotals(ctx context.Context, id identity.WorkloadID, decision innerwallv1.PolicyDecision) ([]Total, error)

ListTotals implements FlowStore.

func (*Postgres) ListWindowPage

func (p *Postgres) ListWindowPage(ctx context.Context, q WindowPageQuery) ([]WindowRow, error)

ListWindowPage implements FlowStore.

func (*Postgres) ListWindows

func (p *Postgres) ListWindows(ctx context.Context, q WindowQuery) ([]WindowRow, error)

ListWindows implements FlowStore.

func (*Postgres) PruneWindows

func (p *Postgres) PruneWindows(ctx context.Context, horizon time.Time) (int64, bool, error)

PruneWindows implements FlowStore. Each batch is its own transaction holding the retention lock, so a run never holds row locks for the whole backlog and another replica can take over between batches. Gaps ride the same batches, and a run continues while either table fills its batch.

func (*Postgres) Rollup

func (p *Postgres) Rollup(ctx context.Context, q RollupQuery) ([]RollupRow, error)

Rollup implements FlowStore.

func (*Postgres) RollupGroups

func (p *Postgres) RollupGroups(ctx context.Context, q GroupQuery) (*GroupResult, error)

RollupGroups implements FlowStore. Each grouping is its own statement; the query's grouping chooses which one runs and nothing is assembled.

func (*Postgres) Within added in v0.5.0

func (p *Postgres) Within(tx pgx.Tx) GapWriter

Within returns a writer of control-plane-derived gaps bound to tx, for a gap that must commit or roll back with the caller's transaction: the silence a workload's retirement closes is written with the retirement itself (ADR-0023). The SQL stays here, behind the interface.

func (*Postgres) WriteGaps added in v0.3.0

func (p *Postgres) WriteGaps(ctx context.Context, id identity.WorkloadID, gaps []Gap) error

WriteGaps implements FlowStore: gaps the control plane derived itself, stored outside any window's transaction.

func (*Postgres) WriteWindow

func (p *Postgres) WriteWindow(ctx context.Context, w Window) (int, error)

WriteWindow implements FlowStore. The rows are copied in one statement and the totals are upserted in one batch, all in one transaction.

type Pruner

type Pruner interface {
	Prune(ctx context.Context, now time.Time) (deleted int64, err error)
}

Pruner is a bounded deletion that rides the retention schedule instead of having a schedule of its own: expired operator sessions are the first (ADR-0021). It reports how many rows it removed. A pruner must be safe to run on every replica at once, because unlike window pruning it is not serialized by the retention lock.

type PrunerFunc

type PrunerFunc func(ctx context.Context, now time.Time) (int64, error)

PrunerFunc adapts a function to Pruner.

func (PrunerFunc) Prune

func (f PrunerFunc) Prune(ctx context.Context, now time.Time) (int64, error)

Prune implements Pruner.

type Record

type Record struct {
	Peer            Peer
	SrcAddress      netip.Addr
	DstAddress      netip.Addr
	DstPort         uint16
	Protocol        innerwallv1.Protocol
	Direction       innerwallv1.Direction
	Decision        innerwallv1.PolicyDecision
	MatchedRuleID   string
	ConnectionCount uint64
	ByteCount       uint64
	FirstSeen       time.Time
	LastSeen        time.Time
	ProcessName     string
}

Record is one aggregated flow observation as the store keeps it: what the agent reported plus the resolved peer.

func AggregateTotals

func AggregateTotals(records []Record) []Record

AggregateTotals folds the records of one window into one record per totals key, which is the shape the upsert needs: a statement may touch a given totals row once, and several records in a window can share a key when two source addresses resolve to the same peer. Counts add, the first-seen instant is the earliest, the last-seen the latest, and the label snapshot, matched rule, and addresses come from the latest-seen record. The result is in a deterministic order.

func (*Record) Key

func (r *Record) Key() TotalsKey

Key returns the totals key of r.

type Retention

type Retention struct {
	Store FlowStore
	// Pruners run after the windows on every round, each in turn; one
	// failing does not stop the others.
	Pruners []Pruner
	// Horizon is the age beyond which windows are deleted;
	// DefaultRetention when zero.
	Horizon time.Duration
	// Interval is the period between runs; DefaultRetentionInterval when
	// zero.
	Interval time.Duration
	Log      *slog.Logger
	// Now is the clock; time.Now if nil.
	Now func() time.Time
}

Retention prunes aged windows on a schedule. Every replica runs one; the store's advisory lock makes sure only one prunes at a time, and a replica that finds the lock held skips the round (ADR-0017). This is a scheduled maintenance job with a fixed period, not a poll for work: it runs whether or not there is anything to delete, and nothing waits on it.

func (*Retention) Run

func (r *Retention) Run(ctx context.Context) error

Run prunes once immediately and then every Interval until ctx ends.

func (*Retention) RunOnce

func (r *Retention) RunOnce(ctx context.Context) (deleted int64, ran bool, err error)

RunOnce performs one pruning run.

type RollupQuery

type RollupQuery struct {
	WorkloadIDs []identity.WorkloadID
	Since       time.Time
	Until       time.Time
	Decision    innerwallv1.PolicyDecision
}

RollupQuery aggregates the windows of a set of workloads (a label scope, resolved by the caller) in [Since, Until) by resolved peer and service. A zero Decision means every decision.

type RollupRow

type RollupRow struct {
	Peer            Peer
	DstPort         uint16
	Protocol        innerwallv1.Protocol
	Decision        innerwallv1.PolicyDecision
	Workloads       int64
	ConnectionCount uint64
	ByteCount       uint64
	FirstSeen       time.Time
	LastSeen        time.Time
}

RollupRow is one group of a rollup: a resolved peer talking to one service (port and protocol) with one decision, across every workload in scope. Workloads counts the distinct workloads that saw it.

type Total

type Total struct {
	WorkloadID      identity.WorkloadID
	Peer            Peer
	DstPort         uint16
	Protocol        innerwallv1.Protocol
	Direction       innerwallv1.Direction
	Decision        innerwallv1.PolicyDecision
	MatchedRuleID   string
	FirstSeen       time.Time
	LastSeen        time.Time
	ConnectionCount uint64
	ByteCount       uint64
	WindowCount     uint64
}

Total is the cumulative record of one flow key since it was first seen. It is upserted at ingest and never pruned, so "since first seen" verdicts come from one row rather than a scan of the windows (ADR-0019).

type TotalsKey

type TotalsKey struct {
	PeerKind  PeerKind
	PeerKey   string
	DstPort   uint16
	Protocol  innerwallv1.Protocol
	Direction innerwallv1.Direction
	Decision  innerwallv1.PolicyDecision
}

TotalsKey identifies one flow_totals row.

type Window

type Window struct {
	WorkloadID identity.WorkloadID
	Start      time.Time
	End        time.Time
	Records    []Record
	Gaps       []Gap
}

Window is one reporting window from one workload, resolved and ready to store, with the evidence gaps that rode it.

type WindowCursor

type WindowCursor struct {
	WindowStart time.Time
	ID          int64
}

WindowCursor is the position after which a page continues: the (window start, id) of the last row served, since pages run newest first.

type WindowPageQuery

type WindowPageQuery struct {
	WorkloadID identity.WorkloadID
	Since      time.Time
	Until      time.Time
	Decision   innerwallv1.PolicyDecision
	Direction  innerwallv1.Direction
	PeerKey    string
	Protocol   innerwallv1.Protocol
	DstPort    uint16
	Before     *WindowCursor
	Limit      int
}

WindowPageQuery selects one page of a workload's windows in [Since, Until), newest first, after Before. A nil Before is the first page. A zero Decision or Direction means any; an empty PeerKey means every peer; a zero Protocol means every service.

type WindowQuery

type WindowQuery struct {
	WorkloadID identity.WorkloadID
	Since      time.Time
	Until      time.Time
	Decision   innerwallv1.PolicyDecision
	Limit      int
}

WindowQuery selects a workload's windows in [Since, Until). A zero Decision means every decision; Limit bounds the rows returned, newest window first.

type WindowRow

type WindowRow struct {
	ID          int64
	WorkloadID  identity.WorkloadID
	WindowStart time.Time
	WindowEnd   time.Time
	Record
}

WindowRow is one stored flow_windows row.

Jump to

Keyboard shortcuts

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