storage

package
v1.18.2 Latest Latest
Warning

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

Go to latest
Published: Oct 13, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Index

Constants

View Source
const (
	AIWarnTokens = "tokens"
	AIWarnCost   = "cost"
)

Which 80% alert MarkAIUsageWarned records.

View Source
const (
	MaxMLBlobBytes   = 256 << 20
	MaxMLBlobKeyLen  = 512
	MLBlobChunkBytes = 4 << 20
)

Limits on a blob. A blob is stored in chunks of MLBlobChunkBytes, which keeps every row or document well under what each backend takes in one (MongoDB's is 16 MB) and keeps a write from holding the whole of a large blob in one statement.

View Source
const (
	MLJobPredict   = "predict"
	MLJobRecommend = "recommend"
	MLJobTrain     = "train"
)

The kinds of an ML job: what it runs.

View Source
const (
	MLJobQueued    = "queued"
	MLJobRunning   = "running"
	MLJobSucceeded = "succeeded"
	MLJobFailed    = "failed"
	MLJobCancelled = "cancelled"
)

The states of an ML job. A job starts queued, is running while a Hermod holds its claim, and ends succeeded, failed or cancelled.

View Source
const (
	MLCallbackPending   = "pending"
	MLCallbackDelivered = "delivered"
	MLCallbackFailed    = "failed"
)

The states of a job's callback. A job without a callback URL has none.

View Source
const (
	MLScoringWorker    = "worker"
	MLScoringInProcess = "in_process"
)

Where a trained model is scored. In-process scoring evaluates the live version's ONNX graph inside Hermod (pkg/ml/onnxscore) when every operator in it is supported, and falls back to the worker when not.

View Source
const (
	DefaultMLDriftWarn    = 0.1
	DefaultMLDriftAlert   = 0.25
	DefaultMLLogRetention = 7 * 24 * time.Hour

	// MaxMLLogRetention is the longest a logged prediction is kept, whatever
	// its model says; the retention sweep applies it to every row, so the
	// log of a model or vhost deleted elsewhere does not outlive it.
	MaxMLLogRetention = 365 * 24 * time.Hour
)

Defaults and bounds of a model's monitoring.

View Source
const (
	MLCallerWorkflow = "workflow" // a Predict node; CallerID is the workflow
	MLCallerREST     = "rest"     // the serving endpoint, with a serving key
	MLCallerGRPC     = "grpc"     // hermod.ml.v1.InferenceService
	MLCallerUI       = "ui"       // an Editor testing the model
	MLCallerMCP      = "mcp"      // a predict_<model> tool on the MCP server
)

Who made a prediction.

View Source
const (
	// MLScheduleDatasetRefresh refills a dataset from a database query.
	MLScheduleDatasetRefresh = "dataset_refresh"
	// MLScheduleModelBuild writes a recommendation model's answers for every
	// salesman, customer or product to a database table.
	MLScheduleModelBuild = "model_build"
)

The kinds of MLSchedule.

View Source
const (
	MaxVHostSecretNameLen  = 128
	MaxVHostSecretValueLen = 64 * 1024
)

Limits on a vhost secret. The name is what an expression writes inside secret("..."), so it is kept to what needs no quoting anywhere.

View Source
const AnyModel = "*"

AnyModel is the price entry for models the price list does not name.

View Source
const MLBackendWorker inference.Backend = "hermod-ml"

MLBackendWorker marks a model trained by Hermod's own hermod-ml worker. Its server is the worker Hermod is configured with, so it holds no URL; its features are its inputs, one tensor each; and RemoteVersion is the version that is live, empty until one is put live.

View Source
const MLJobResultStored = "stored"

MLJobResultStored is a job's ResultRef when its result was too large to keep on the job and is kept apart; GetMLJobResult reads it.

View Source
const MLTaskRecommendation = "recommendation"

MLTaskRecommendation is the task of a trained model that recommends items to people. Hermod serves it in-process from its artifact (pkg/ml/recommend).

View Source
const MLWorkerSettingKey = "hermod.ml_worker"

MLWorkerSettingKey names the settings row holding the ML worker connected from the console: a JSON object of strings, "url" and "token", whose token is encrypted as a connector's credentials are (configsecrets).

View Source
const MaxMLJobPage = 200

MaxMLJobPage bounds one page of a job listing.

View Source
const MaxMLModelNameLen = 64

MaxMLModelNameLen bounds a model name, which appears in URLs and metrics.

View Source
const MaxMLPredictionLogPage = 500

MaxMLPredictionLogPage bounds one read of a model's prediction log.

View Source
const MaxMLScriptBytes = 256 << 10

MaxMLScriptBytes bounds a script's source; the worker refuses larger ones.

View Source
const (

	// TraceDedupCacheSize is how many (workflow, message, payload) facts are
	// remembered at once. A message's steps are recorded within milliseconds of
	// each other, so the useful lifetime of an entry is short and a few
	// thousand slots cover far more concurrency than the trace recorder's own
	// slot limit allows. The bound is not optional: the key contains a message
	// id, which is whatever the source supplied.
	TraceDedupCacheSize = 8192
)

Variables

View Source
var (
	WorkflowSearchFields  = []string{"id", "name", "vhost"}
	ConnectorSearchFields = []string{"id", "name", "type", "vhost"}
	UserSearchFields      = []string{"id", "username", "full_name", "email", "role"}
	VHostSearchFields     = []string{"id", "name", "description"}
	WorkerSearchFields    = []string{"id", "name", "host", "description"}

	// LogSearchFields deliberately omits data. It is the largest column on the
	// largest table Hermod owns, a LIKE over it cannot use an index, and a log
	// search that scans every payload is a log search nobody waits for. The
	// mongo backend used to include it; this is the one place the two were
	// reconciled by taking the narrower list rather than the union.
	LogSearchFields = []string{"message", "action", "source_id", "sink_id", "workflow_id", "user_id", "username"}

	AuditLogSearchFields = []string{"id", "username", "action", "entity_id", "payload"}
)

What each list search matches.

These live here rather than in the backends because they were hand-mirrored in two packages and drifted: workflows matched name alone in SQL and _id, name and vhost in mongo; users matched role in SQL and not in mongo; workers matched description in SQL and not in mongo; logs matched action in SQL and data, user_id and username in mongo. Which backend was deployed therefore changed what the search box found, and nothing failed when the two lists disagreed — the only way to notice was to run both and compare.

One list per entity, read by both backends, is what makes that impossible rather than merely fixed. Fields are named as the SQL column; the mongo backend renames id to _id on the way past, in searchAcross, so no caller has to remember to.

View Source
var EncryptedSettingKeys = []string{MLWorkerSettingKey}

EncryptedSettingKeys are the settings whose value is a JSON object of strings with its credentials encrypted by configsecrets. ReEncryptSecrets rewrites them with a connector's config, so rotating the master key does not strand them.

View Source
var ErrAIBudgetsUnsupported = errors.New("this storage backend cannot hold AI budgets")

ErrAIBudgetsUnsupported is returned when the configured backend cannot store AI budgets.

View Source
var ErrApprovalDecided = errors.New("approval was already decided")

ErrApprovalDecided is returned when an approval is no longer pending, so a second decision cannot resume its workflow again.

View Source
var ErrMLBlobExists = errors.New("the ML blob already exists")

ErrMLBlobExists is CreateMLBlob's answer when the key is taken.

View Source
var ErrMLJobClaimLost = errors.New("the claim on this ML job was lost")

ErrMLJobClaimLost is returned to a runner that no longer holds a job's claim: it expired and another Hermod took the job, or the job was removed.

View Source
var ErrMLJobsUnsupported = errors.New("this storage backend cannot hold ML jobs")

ErrMLJobsUnsupported is returned when the configured backend cannot store ML jobs.

View Source
var ErrMLModelsUnsupported = errors.New("this storage backend cannot hold ML models")

ErrMLModelsUnsupported is returned when the configured backend cannot store models.

View Source
var ErrMLPredictionLogsUnsupported = errors.New("this storage backend cannot hold ML prediction logs")

ErrMLPredictionLogsUnsupported is returned when the log store cannot hold prediction logs.

View Source
var ErrMLQuotasUnsupported = errors.New("this storage backend cannot hold ML quotas")

ErrMLQuotasUnsupported is returned when the configured backend cannot store quotas.

View Source
var ErrNotFound = errors.New("not found")
View Source
var ErrVHostSecretsUnsupported = errors.New("this storage backend does not support vhost secrets")

ErrVHostSecretsUnsupported is returned by a storage backend that has no vhosts to hold secrets for.

View Source
var MLFeedbackOutcomes = []string{"shown", "accepted", "dismissed", "visited", "ordered"}

The outcomes of a recommendation that feedback reports.

View Source
var MLMaskTypes = []string{"all", "partial", "email", "pii"}

MLMaskTypes are the ways a logged field can be masked: the mask transformer's (pkg/comm/transformer/security), "all" replacing the value.

View Source
var NodeConfigSourceKeys = []string{"sourceId", "sourceID"}

NodeConfigSourceKeys are the config keys through which a node names a source without being a source node. db_lookup writes "sourceId"; the enrichment SQL node (SQLConfig.tsx) reads and writes both spellings.

View Source
var SharedVHosts = []string{"", "default", "all"}

SharedVHosts are the vhost names every user may access.

Functions

func AIUsageID added in v1.18.0

func AIUsageID(vhost, period, workflowID string) string

AIUsageID is a usage row's key. The vhost's length is in it, so no vhost and workflow pair can spell another's key whatever they contain.

func AIUsagePeriod added in v1.18.0

func AIUsagePeriod(t time.Time) string

AIUsagePeriod is the usage month t falls in: its calendar month in UTC.

func CostToMicros added in v1.18.0

func CostToMicros(cost float64) int64

CostToMicros converts a cost limit to the unit usage is counted in.

func DecodeTracePayload added in v1.10.0

func DecodeTracePayload(blob []byte) ([]byte, error)

decodeTracePayload recovers the JSON behind a stored after_blob value.

func EncodeTracePayload added in v1.10.0

func EncodeTracePayload(raw []byte) []byte

encodeTracePayload turns marshalled JSON into the bytes stored in after_blob.

func MLBlobChunks added in v1.18.2

func MLBlobChunks(data []byte) [][]byte

MLBlobChunks splits data into the chunks it is stored as: at least one, so an empty blob still has a row.

func MLJobTerminal added in v1.18.0

func MLJobTerminal(status string) bool

MLJobTerminal reports whether a job in status has finished.

func MLPredictionLogLimit added in v1.18.0

func MLPredictionLogLimit(limit int) int

MLPredictionLogLimit bounds a requested page size.

func MLScriptSHA256 added in v1.18.0

func MLScriptSHA256(source string) string

MLScriptSHA256 is the hex SHA-256 of a script's source, its version identity.

func NormalizeVHost added in v1.8.1

func NormalizeVHost(vhost string) string

NormalizeVHost maps the two spellings of "no vhost filter" onto one.

The UI sends vhost=all for the unfiltered view while GetDashboardStats treats an empty string the same way. Storing history under both spellings would split one series into two, so every read and write of a sample goes through here.

func TracePayloadHash added in v1.10.0

func TracePayloadHash(raw []byte) []byte

tracePayloadHash identifies a payload by its content, within one message.

128 bits of SHA-256, not 64. The bytes hashed are the message's own data, which in a CDC pipeline is whatever the upstream database contained and in a webhook pipeline is whatever the caller sent. A 64-bit content address over input someone else chooses is a 2^32 search away from showing one node's payload under another node's name — and a trace viewer that can be made to lie about which node produced what is worse than a slightly larger table.

func TracePayloadKey added in v1.10.0

func TracePayloadKey(raw []byte) string

TracePayloadKey is TracePayloadHash rendered as hex.

The document stores keep their payloads in a map, and a map key has to be a string there: a BSON field name, or a JSON object key. Keying that map by content is what makes dedup inherent for those backends rather than something a cache has to get right — writing the same payload twice writes the same key twice, which is a no-op.

func ValidMLModelName added in v1.18.0

func ValidMLModelName(name string) bool

ValidMLModelName reports whether name is a letter followed by letters, digits, '_' or '-', within the length limit. It is used in URL paths, so it may hold nothing that changes a path's meaning.

func ValidVHostSecretName added in v1.17.0

func ValidVHostSecretName(name string) bool

ValidVHostSecretName reports whether name is [A-Za-z_][A-Za-z0-9_]*, within the length limit.

func ValidateAIBudget added in v1.18.0

func ValidateAIBudget(b AIBudget) error

ValidateAIBudget reports what is wrong with a budget before it is saved.

func ValidateMLBlob added in v1.18.2

func ValidateMLBlob(vhost, key string, size int) error

ValidateMLBlob reports what is wrong with a blob's vhost, key or size before it is stored. A key is non-empty segments joined by "/", none of "." or "..", with no control characters.

func ValidateMLBlobPrefix added in v1.18.2

func ValidateMLBlobPrefix(vhost, prefix string) error

ValidateMLBlobPrefix reports what is wrong with a prefix to list or delete by: "" (every blob of the vhost), or a key's leading segments ending in "/".

func ValidateMLFeedbackEvent added in v1.18.0

func ValidateMLFeedbackEvent(e MLFeedbackEvent) error

ValidateMLFeedbackEvent reports what is wrong with one event.

func ValidateMLModel added in v1.18.0

func ValidateMLModel(m MLModel) error

ValidateMLModel reports what is wrong with a model before it is saved.

func ValidateMLQuotas added in v1.18.0

func ValidateMLQuotas(q MLQuotas) error

ValidateMLQuotas reports what is wrong with a vhost's quotas before they are saved.

func ValidateMLSchedule added in v1.18.0

func ValidateMLSchedule(s MLSchedule) error

ValidateMLSchedule reports what is wrong with a schedule before it is saved. The cron expression itself is checked by its user.

func ValidateMLScoring added in v1.18.0

func ValidateMLScoring(s string) error

ValidateMLScoring reports whether s is a scoring mode; empty is the default, the worker.

func ValidateMLScript added in v1.18.0

func ValidateMLScript(s MLScript) error

ValidateMLScript reports what is wrong with a script before it is saved. Names follow the model name rule: they appear in URLs and in the algorithm a training names ("custom:<name>").

func ValidateVHostSecret added in v1.17.0

func ValidateVHostSecret(s VHostSecret) error

ValidateVHostSecret reports what is wrong with a secret about to be saved.

func WorkflowQueriesSource added in v1.8.1

func WorkflowQueriesSource(wf Workflow, sourceID string, delegateOf func(id string) string) bool

WorkflowQueriesSource reports whether a workflow aims SQL at this source without holding it in a source node's RefID: a db_lookup's sourceId, or the delegate of a batch_sql source it does hold.

Walking nodes for `type == "source"` alone misses both, which is how a source that a running workflow depends on could be edited or deleted out from under it. delegateOf resolves a source id to its batch_sql delegate, or "" when it has none; callers that cannot resolve sources may pass nil.

Types

type AIBudget added in v1.18.0

type AIBudget struct {
	VHost string `json:"vhost"`

	// Disabled is the kill switch: while it is on, no AI node or agent in
	// the vhost may call a model.
	Disabled bool `json:"disabled"`

	MonthlyTokens int64   `json:"monthly_tokens,omitempty"`
	MonthlyCost   float64 `json:"monthly_cost,omitempty"`
	// Currency labels the costs (for example USD). It is not converted.
	Currency string `json:"currency,omitempty"`

	// Prices are per million tokens. The model "*" prices every model the
	// list does not name; a cost limit needs it, so no call goes uncounted.
	Prices []AIModelPrice `json:"prices,omitempty"`

	Workflows []AIWorkflowCap `json:"workflows,omitempty"`

	UpdatedBy string    `json:"updated_by,omitempty"`
	UpdatedAt time.Time `json:"updated_at,omitzero"`
}

AIBudget is a vhost's spending policy for language model calls: a kill switch, a monthly budget for the whole vhost, and monthly caps for single workflows. Months are calendar months in UTC. A zero limit is no limit.

Costs are worked out from the prices set here, in whatever currency the vhost bills in; Hermod holds no price list of its own, since model ids and their prices are configuration that changes faster than a release.

func (AIBudget) CostMicros added in v1.18.0

func (b AIBudget) CostMicros(model string, inputTokens, outputTokens int64) int64

CostMicros is what a call to model with these token counts cost, in millionths of the budget's currency. A model with no price costs 0.

func (AIBudget) HasCostLimit added in v1.18.0

func (b AIBudget) HasCostLimit() bool

HasCostLimit reports whether the vhost or any workflow cap limits cost.

func (AIBudget) Limited added in v1.18.0

func (b AIBudget) Limited() bool

Limited reports whether the budget can refuse a call at all.

func (AIBudget) Price added in v1.18.0

func (b AIBudget) Price(model string) (AIModelPrice, bool)

Price returns the price of model: its own entry, else the "*" entry.

func (AIBudget) WorkflowCap added in v1.18.0

func (b AIBudget) WorkflowCap(workflowID string) (AIWorkflowCap, bool)

WorkflowCap returns the cap set for workflowID, if there is one.

type AIBudgetStore added in v1.18.0

type AIBudgetStore interface {
	// GetAIBudget returns the vhost's budget, or ErrNotFound.
	GetAIBudget(ctx context.Context, vhost string) (AIBudget, error)
	// PutAIBudget creates or replaces the vhost's budget.
	PutAIBudget(ctx context.Context, b AIBudget) error
	// AddAIUsage adds d to a scope's usage for the period atomically, creating
	// the row on first use, and returns the totals after the addition.
	AddAIUsage(ctx context.Context, vhost, period, workflowID string, d AIUsageDelta) (AIUsage, error)
	// GetAIUsage returns a scope's usage for the period; zero when it has none.
	GetAIUsage(ctx context.Context, vhost, period, workflowID string) (AIUsage, error)
	// ListAIUsage returns every scope of the vhost with usage in the period,
	// the vhost's own row (no workflow) first and then by workflow id.
	ListAIUsage(ctx context.Context, vhost, period string) ([]AIUsage, error)
	// MarkAIUsageWarned records that kind's 80% alert went out for a scope
	// and period. It reports true to exactly one caller however many race,
	// replicas included, so the alert is raised once.
	MarkAIUsageWarned(ctx context.Context, vhost, period, workflowID, kind string) (bool, error)
	// DeleteAIBudgets removes the vhost's budget and all of its usage.
	DeleteAIBudgets(ctx context.Context, vhost string) error
}

AIBudgetStore is implemented by a storage backend that can hold AI budgets and usage. Like MLModelStore it is separate from Storage; callers find it with a type assertion.

type AIModelPrice added in v1.18.0

type AIModelPrice struct {
	Model            string  `json:"model"`
	InputPerMillion  float64 `json:"input_per_million"`
	OutputPerMillion float64 `json:"output_per_million"`
}

AIModelPrice is what one model costs per million tokens.

type AIUsage added in v1.18.0

type AIUsage struct {
	VHost        string `json:"vhost"`
	Period       string `json:"period"`
	WorkflowID   string `json:"workflow_id,omitempty"`
	Calls        int64  `json:"calls"`
	InputTokens  int64  `json:"input_tokens"`
	OutputTokens int64  `json:"output_tokens"`
	CostMicros   int64  `json:"cost_micros"`
	// TokensWarned and CostWarned record that the 80% alert went out.
	TokensWarned bool      `json:"tokens_warned,omitempty"`
	CostWarned   bool      `json:"cost_warned,omitempty"`
	UpdatedAt    time.Time `json:"updated_at,omitzero"`
}

AIUsage is what one scope spent in one month: the vhost as a whole when WorkflowID is empty, one workflow otherwise.

func (AIUsage) Tokens added in v1.18.0

func (u AIUsage) Tokens() int64

Tokens is input and output together, which is what a token limit counts.

type AIUsageDelta added in v1.18.0

type AIUsageDelta struct {
	InputTokens  int64
	OutputTokens int64
	CostMicros   int64
}

AIUsageDelta is one call's addition to a usage row.

type AIWorkflowCap added in v1.18.0

type AIWorkflowCap struct {
	WorkflowID    string  `json:"workflow_id"`
	MonthlyTokens int64   `json:"monthly_tokens,omitempty"`
	MonthlyCost   float64 `json:"monthly_cost,omitempty"`
}

AIWorkflowCap is one workflow's monthly limit, within the vhost's.

type Approval

type Approval struct {
	ID             string            `json:"id"`
	WorkflowID     string            `json:"workflow_id"`
	NodeID         string            `json:"node_id"`
	MessageID      string            `json:"message_id"`
	Payload        []byte            `json:"payload"`
	Metadata       map[string]string `json:"metadata"`
	Data           map[string]any    `json:"data"`
	FormDefinition map[string]any    `json:"form_definition,omitempty"`
	FormData       map[string]any    `json:"form_data,omitempty"`
	Status         string            `json:"status"` // pending, approved, rejected
	CreatedAt      time.Time         `json:"created_at"`
	ProcessedAt    *time.Time        `json:"processed_at,omitempty"`
	ProcessedBy    string            `json:"processed_by,omitempty"`
	Notes          string            `json:"notes,omitempty"`
}

type ApprovalFilter

type ApprovalFilter struct {
	CommonFilter
	WorkflowID string
	Status     string
	// VHosts, when not nil, limits the list to approvals raised by workflows
	// in these vhosts or in the shared default vhost ("", "default", "all"),
	// matching Handler.HasVHostAccess. Nil means no limit.
	VHosts []string
}

type AuditFilter

type AuditFilter struct {
	CommonFilter
	UserID     string     `json:"user_id"`
	EntityType string     `json:"entity_type"`
	EntityID   string     `json:"entity_id"`
	Action     string     `json:"action"`
	From       *time.Time `json:"from" omitzero:"true"`
	To         *time.Time `json:"to" omitzero:"true"`
}

type AuditLog

type AuditLog struct {
	ID         string    `json:"id"`
	Timestamp  time.Time `json:"timestamp"`
	UserID     string    `json:"user_id"`
	Username   string    `json:"username"`
	Action     string    `json:"action"`      // e.g., "CREATE", "UPDATE", "DELETE", "START", "STOP"
	EntityType string    `json:"entity_type"` // e.g., "workflow", "source", "sink", "user"
	EntityID   string    `json:"entity_id"`
	Payload    string    `json:"payload,omitempty"` // Details about the change (JSON)
	IP         string    `json:"ip,omitempty"`
}

type CommonFilter

type CommonFilter struct {
	Page        int
	Limit       int
	Search      string
	VHost       string
	WorkspaceID string
	// WithoutWorkspace limits matches to rows with no workspace — the view you
	// need to find what is left to organise. Mirrors LogFilter.WithoutWorkflow.
	// It takes precedence over WorkspaceID, which is never set at the same time.
	WithoutWorkspace bool
	// Since and Until bound time-based queries (e.g., logs). Zero value means not set.
	Since time.Time `json:"since" omitzero:"true"`
	Until time.Time `json:"until" omitzero:"true"`

	// Workflow filters
	WorkerID string `json:"worker_id,omitempty"`
	OwnerID  string `json:"owner_id,omitempty"`
	Active   *bool  `json:"active,omitempty"`
}

type DashboardSample added in v1.8.1

type DashboardSample struct {
	Timestamp       time.Time `json:"timestamp"`
	VHost           string    `json:"vhost"`
	Throughput      float64   `json:"throughput"`
	TotalProcessed  uint64    `json:"total_processed"`
	TotalErrors     uint64    `json:"total_errors"`
	TotalLag        uint64    `json:"total_lag"`
	ErrorRate       float64   `json:"error_rate"`
	AvgLatencyMs    float64   `json:"avg_latency_ms"`
	ActiveWorkflows int       `json:"active_workflows"`
	ActiveWorkers   int       `json:"active_workers"`
}

DashboardSample is one point of dashboard history.

The throughput chart used to be built entirely in the browser: the page held the last thirty WebSocket readings in React state and threw them away on reload. That makes a reload indistinguishable from an outage — the chart restarts at zero either way — and means nobody can answer "was it like this an hour ago?". Persisting a sample per interval is what turns that sliver into a series the chart can be rebuilt from.

It is deliberately a narrow projection of DashboardStats rather than the whole struct: configuration counts (TotalSources, TotalWorkflows) describe what exists rather than what happened, and storing them once per interval would be a lot of rows to say nothing. Only the fields that move over time are kept, and none of them are message payloads, so no customer data enters this table.

type DashboardStats

type DashboardStats struct {
	ActiveSources   int     `json:"active_sources"`
	ActiveSinks     int     `json:"active_sinks"`
	ActiveWorkflows int     `json:"active_workflows"`
	TotalProcessed  uint64  `json:"total_processed"`
	TotalErrors     uint64  `json:"total_errors"`
	TotalLag        uint64  `json:"total_lag"`
	FailedWorkflows int     `json:"failed_workflows"`
	Uptime          int64   `json:"uptime"`
	ActiveWorkers   int     `json:"active_workers"`
	TotalWorkflows  int     `json:"total_workflows"`
	TotalSources    int     `json:"total_sources"`
	TotalSinks      int     `json:"total_sinks"`
	Throughput      float64 `json:"throughput"` // Messages per second

	// Health signals. These answer "is the data moving well?", which the
	// counters above cannot: a pipeline with a rising TotalProcessed and a
	// wedged sink looks identical to a healthy one until someone reads the
	// logs.
	//
	// There is deliberately no DeadLetterCount here. TotalErrors is already
	// exactly that — flushStatsToStorage persists baseErrors +
	// StatusUpdate.DeadLetterCount into total_errors — and showing the same
	// number twice under two names is how a dashboard loses the reader's
	// trust.
	//
	// ErrorRate is derived from the persisted counters and so is cumulative
	// over the workflow's life. The other three are point-in-time readings of
	// engines running on this node, so they reset when an engine restarts and
	// read zero when nothing is running.
	ErrorRate           float64 `json:"error_rate"`     // Dead-lettered share of attempted messages, 0..1
	AvgLatencyMs        float64 `json:"avg_latency_ms"` // Mean end-to-end latency across running engines
	Backpressure        float64 `json:"backpressure"`   // Worst sink buffer fill, 0..1
	CircuitBreakersOpen int     `json:"circuit_breakers_open"`

	// Cluster capacity and utilisation, over exactly the workers ActiveWorkers
	// counts — those that reported a heartbeat inside the last two minutes. A
	// worker that stopped reporting has already been excluded from the count,
	// and counting its cores and disk would describe machines that are no
	// longer there.
	//
	// Totalled rather than averaged, because the question is "how much machine
	// is behind this platform, and how much of it is gone". An average hides a
	// node that is full next to one that is idle, and the full one is the
	// reason anybody opened the page.
	//
	// Workers that report no capacity contribute nothing rather than dragging a
	// total down — see WorkerResources. And one worker per host is assumed:
	// two workers on one machine would count that machine's memory and disk
	// twice. Hermod's topology is a worker per node, and the alternative,
	// deduplicating on a self-reported hostname, is wrong in exactly the
	// container case it would exist to handle.
	CPUCores          int     `json:"cpu_cores"`
	CPUUsage          float64 `json:"cpu_usage"` // 0..1, weighted by each worker's core count
	MemoryTotalBytes  int64   `json:"memory_total_bytes"`
	MemoryUsedBytes   int64   `json:"memory_used_bytes"`
	StorageTotalBytes int64   `json:"storage_total_bytes"`
	StorageUsedBytes  int64   `json:"storage_used_bytes"`
}

type FormSubmission

type FormSubmission struct {
	ID        string    `json:"id"`
	Timestamp time.Time `json:"timestamp" omitzero:"true"`
	Path      string    `json:"path"`
	Data      []byte    `json:"data"`
	Status    string    `json:"status"` // pending, processing, completed, failed
}

type FormSubmissionFilter

type FormSubmissionFilter struct {
	CommonFilter
	Path   string `json:"path"`
	Status string `json:"status"`
}

type LineageEdge

type LineageEdge struct {
	SourceID     string `json:"source_id"`
	SourceName   string `json:"source_name"`
	SourceType   string `json:"source_type"`
	SinkID       string `json:"sink_id"`
	SinkName     string `json:"sink_name"`
	SinkType     string `json:"sink_type"`
	WorkflowID   string `json:"workflow_id"`
	WorkflowName string `json:"workflow_name"`
}

type Log

type Log struct {
	ID         string    `json:"id"`
	Timestamp  time.Time `json:"timestamp"`
	Level      string    `json:"level"`
	Message    string    `json:"message"`
	Action     string    `json:"action,omitempty"`
	SourceID   string    `json:"source_id,omitempty"`
	SinkID     string    `json:"sink_id,omitempty"`
	WorkflowID string    `json:"workflow_id,omitempty"`
	UserID     string    `json:"user_id,omitempty"`
	Username   string    `json:"username,omitempty"`
	Data       string    `json:"data,omitempty"`
}

type LogFilter

type LogFilter struct {
	CommonFilter
	SourceID   string
	SinkID     string
	WorkflowID string
	Level      string
	Action     string
	// WithoutWorkflow limits matches to logs without a workflow_id (NULL).
	WithoutWorkflow bool
}

type MLBlob added in v1.18.2

type MLBlob struct {
	Key       string    `json:"key"`
	Size      int64     `json:"size"`
	UpdatedAt time.Time `json:"updated_at"`
	Data      []byte    `json:"-"`
}

MLBlob is one named blob of Hermod's built-in ML engine: a dataset part, a model version's meta or its artifact (pkg/ml/builtin). Keeping them in the metadata store, rather than on one server's disk, is what lets every Hermod replica train on a dataset another one filled and score a model another one trained, and keeps them across a restart onto a fresh disk.

A blob belongs to one vhost and is named by a key of path-like segments ("models/churn/b3/meta"). Data is empty in a listing made without it.

type MLBlobStore added in v1.18.2

type MLBlobStore interface {
	// PutMLBlob stores data under key, replacing any blob there. A reader
	// sees the old blob or the new one, never a mix.
	PutMLBlob(ctx context.Context, vhost, key string, data []byte) error
	// CreateMLBlob stores data under key only if nothing is there yet, and
	// returns ErrMLBlobExists otherwise: two servers claiming the same key
	// cannot both succeed.
	CreateMLBlob(ctx context.Context, vhost, key string, data []byte) error
	// GetMLBlob returns the blob under key, or ErrNotFound.
	GetMLBlob(ctx context.Context, vhost, key string) ([]byte, error)
	// ListMLBlobs returns the blobs whose key starts with prefix ("" for
	// all), ordered by key, with their data only when withData is set.
	ListMLBlobs(ctx context.Context, vhost, prefix string, withData bool) ([]MLBlob, error)
	// DeleteMLBlobs removes the blobs whose key starts with prefix ("" for
	// every blob of the vhost) and returns how many there were.
	DeleteMLBlobs(ctx context.Context, vhost, prefix string) (int, error)
}

MLBlobStore is implemented by a metadata store that keeps the built-in ML engine's blobs. Like MLModelStore it is separate from Storage; callers find it with a type assertion.

type MLFeedbackEvent added in v1.18.0

type MLFeedbackEvent struct {
	VHost            string    `json:"vhost"`
	Model            string    `json:"model"`
	RecommendationID string    `json:"recommendation_id"`
	ItemID           string    `json:"id"`
	Outcome          string    `json:"outcome"`
	At               time.Time `json:"at"`
}

MLFeedbackEvent is what happened to one recommended item: shown to someone, accepted, dismissed, visited or ordered. RecommendationID is the answer it was in.

type MLFeedbackStore added in v1.18.0

type MLFeedbackStore interface {
	// AddMLFeedback stores events, every one or none.
	AddMLFeedback(ctx context.Context, events []MLFeedbackEvent) error
	// CountMLFeedback counts a model's events at or after since, by outcome.
	CountMLFeedback(ctx context.Context, vhost, model string, since time.Time) (map[string]int, error)
	// DeleteMLFeedback removes a model's feedback, or the whole vhost's when
	// model is empty.
	DeleteMLFeedback(ctx context.Context, vhost, model string) error
}

MLFeedbackStore is implemented by a storage backend that keeps feedback on recommendations. Like MLModelStore, callers find it with a type assertion.

type MLGoLive added in v1.18.0

type MLGoLive struct {
	Mode string `json:"mode"`
	// Metric is the metric "if" checks; empty means "score", which is
	// accuracy for a classifier and R² for a regression.
	Metric string   `json:"metric,omitempty"`
	Min    *float64 `json:"min,omitempty"`
	Max    *float64 `json:"max,omitempty"`
}

MLGoLive says whether a newly trained version is put live: "always", "never" (the zero value), or "if" one of its metrics is within Min and Max. internal/ml decides with it; it is here so a retrain policy can hold one.

type MLJob added in v1.18.0

type MLJob struct {
	ID    string `json:"id"`
	VHost string `json:"vhost"`
	Model string `json:"model"`
	Kind  string `json:"kind"`

	Status string `json:"status"`
	// Progress is how far along the job is, 0 to 1, and ProgressMessage
	// says so in words.
	Progress        float64 `json:"progress"`
	ProgressMessage string  `json:"progress_message,omitempty"`

	// Request is what the job runs, as JSON; its shape depends on Kind.
	// Listings leave it out.
	Request json.RawMessage `json:"request,omitempty"`
	// Result is the answer, as JSON, when it is small enough to keep here.
	// ResultRef is MLJobResultStored when it is kept apart instead.
	// Listings leave both out.
	Result    json.RawMessage `json:"result,omitempty"`
	ResultRef string          `json:"result_ref,omitempty"`
	Error     string          `json:"error,omitempty"`

	// CallbackURL is told when the job ends. CallbackStatus is pending until
	// it has been, delivered or failed after; empty without a URL.
	CallbackURL       string    `json:"callback_url,omitempty"`
	CallbackStatus    string    `json:"callback_status,omitempty"`
	CallbackAttempts  int       `json:"callback_attempts,omitempty"`
	CallbackNextAt    time.Time `json:"-"`
	CallbackLastError string    `json:"callback_error,omitempty"`

	// CallerKind is who submitted it: one of the MLCaller kinds. A job
	// submitted with a serving key (MLCallerREST or MLCallerGRPC) is the
	// only kind that key may read. CreatedBy is the user, when there is one.
	CallerKind string `json:"caller_kind"`
	CreatedBy  string `json:"created_by,omitempty"`

	// CancelRequested asks the Hermod running the job to stop it.
	CancelRequested bool `json:"cancel_requested,omitempty"`
	// Attempts is how many times the job was claimed to run. More than one
	// means a Hermod running it stopped and another took it over.
	Attempts int `json:"attempts"`
	// ClaimedBy holds the job, while it runs or its callback is sent, until
	// ClaimUntil; a claim not renewed by then may be taken over.
	ClaimedBy  string    `json:"-"`
	ClaimUntil time.Time `json:"-"`

	CreatedAt  time.Time  `json:"created_at"`
	StartedAt  *time.Time `json:"started_at,omitempty"`
	FinishedAt *time.Time `json:"finished_at,omitempty"`
	// ExpiresAt is when the job and its result are removed.
	ExpiresAt time.Time `json:"expires_at"`
}

MLJob is one asynchronous call of a model: a prediction, recommendations or a training, queued by one Hermod and run by whichever claims it.

type MLJobFilter added in v1.18.0

type MLJobFilter struct {
	VHost  string
	Model  string
	Status string
	Limit  int
	Offset int
}

MLJobFilter selects a vhost's jobs. Model and Status are optional. Limit is the page size and Offset where the page starts.

type MLJobFinish added in v1.18.0

type MLJobFinish struct {
	Status    string
	Result    json.RawMessage
	ResultRef string
	Error     string
	At        time.Time
}

MLJobFinish is how a job ended, as the runner holding it records it.

type MLJobStore added in v1.18.0

type MLJobStore interface {
	// CreateMLJob stores a new job as it is given.
	CreateMLJob(ctx context.Context, j MLJob) error
	// GetMLJob returns one job of the vhost, with its request and result, or
	// ErrNotFound.
	GetMLJob(ctx context.Context, vhost, id string) (MLJob, error)
	// ListMLJobs returns one page of the vhost's jobs, newest first, without
	// their requests and results, and how many match in all.
	ListMLJobs(ctx context.Context, f MLJobFilter) ([]MLJob, int, error)
	// ClaimMLJob takes the oldest job that is queued, or running under a
	// claim that expired before now, for owner until now+ttl, marks it
	// running and counts the attempt. It reports false when there is none.
	ClaimMLJob(ctx context.Context, owner string, ttl time.Duration, now time.Time) (MLJob, bool, error)
	// UpdateMLJobProgress records how far owner's running job is and renews
	// its claim until until. It reports whether cancelling the job was
	// asked for, and ErrMLJobClaimLost when owner no longer holds it.
	UpdateMLJobProgress(ctx context.Context, id, owner string, progress float64, message string, until time.Time) (bool, error)
	// FinishMLJob records how owner's job ended and releases it. A job with
	// a callback URL is then due for its callback. It returns
	// ErrMLJobClaimLost when owner no longer holds it.
	FinishMLJob(ctx context.Context, id, owner string, f MLJobFinish) error
	// CancelMLJob cancels a queued job at once, or asks the Hermod running a
	// running one to stop it, and returns the job. A finished job is
	// returned as it is. It returns ErrNotFound for no such job.
	CancelMLJob(ctx context.Context, vhost, id string, now time.Time) (MLJob, error)
	// ClaimMLJobCallback takes, for owner until now+ttl, a finished job whose
	// callback is pending and due. It reports false when there is none.
	ClaimMLJobCallback(ctx context.Context, owner string, ttl time.Duration, now time.Time) (MLJob, bool, error)
	// RecordMLJobCallback records one delivery attempt of owner's callback
	// and releases the job: status is pending again (with next, when to try
	// next), delivered or failed.
	RecordMLJobCallback(ctx context.Context, id, owner, status string, attempts int, next time.Time, lastError string) error
	// PutMLJobResult keeps a result too large for the job itself.
	PutMLJobResult(ctx context.Context, vhost, id string, result json.RawMessage) error
	// GetMLJobResult reads a result PutMLJobResult kept, or ErrNotFound.
	GetMLJobResult(ctx context.Context, vhost, id string) (json.RawMessage, error)
	// DeleteExpiredMLJobs removes every job, and its result, that expired
	// before now, and returns how many jobs went.
	DeleteExpiredMLJobs(ctx context.Context, now time.Time) (int, error)
	// DeleteMLJobs removes every job, and result, the vhost holds.
	DeleteMLJobs(ctx context.Context, vhost string) error
}

MLJobStore is implemented by a storage backend that can hold ML jobs. Like MLModelStore it is separate from Storage; callers find it with a type assertion.

type MLModel added in v1.18.0

type MLModel struct {
	VHost       string `json:"vhost"`
	Name        string `json:"name"`
	Description string `json:"description,omitempty"`

	// Backend, URL, RemoteModel and RemoteVersion are inference.Target's
	// Backend, URL, Model and Version.
	Backend       inference.Backend `json:"backend"`
	URL           string            `json:"url"`
	RemoteModel   string            `json:"remote_model,omitempty"`
	RemoteVersion string            `json:"remote_version,omitempty"`

	// TokenSecret names a secret of the same vhost whose value is sent as a
	// bearer token. The token itself is never stored on the model.
	TokenSecret string `json:"token_secret,omitempty"`

	InputName string   `json:"input_name,omitempty"`
	Features  []string `json:"features,omitempty"`
	// FeatureTypes is feature -> number, string or bool, where it is known:
	// for a model Hermod trained, from the dataset it was trained on.
	FeatureTypes map[string]string `json:"feature_types,omitempty"`
	TimeoutMs    int               `json:"timeout_ms,omitempty"`

	// MCPExposed offers the model as a predict_<name> tool on Hermod's MCP
	// server to callers who may use its vhost. Off unless someone turns it on.
	MCPExposed bool `json:"mcp_exposed"`

	// ServingKeyHash is the SHA-256 of the key an application presents to call
	// the model through the serving endpoints. Empty means serving is off. The
	// key itself is shown once, when it is made, and never stored.
	ServingKeyHash string `json:"-"`
	// Serving reports whether a serving key is set; it is what the API shows.
	Serving bool `json:"serving"`

	// Monitoring is how the model's predictions are logged and its drift
	// judged. The zero value logs nothing and uses the default thresholds.
	Monitoring MLMonitoring `json:"monitoring"`
	// Retrain is when a trained model trains again by itself, nil for never.
	// RetrainStatus is what its last training did. PutMLModel changes
	// neither; MLRetrainStore does.
	Retrain       *MLRetrainPolicy `json:"retrain,omitempty"`
	RetrainStatus *MLRetrainStatus `json:"retrain_status,omitempty"`

	// Scoring is where a trained model's predictions are computed:
	// MLScoringWorker (also the meaning of empty) or MLScoringInProcess.
	// PutMLModel does not change it; MLScoringStore does.
	Scoring string `json:"scoring,omitempty"`

	// Task is what a model Hermod trained does, as its live version (or,
	// before one is live, its latest) says: classification, regression or
	// MLTaskRecommendation. A recommender is called with a person or an
	// item, not rows. Empty for a model served elsewhere, and for one
	// trained before the task was kept.
	Task string `json:"task,omitempty"`

	UpdatedBy string    `json:"updated_by,omitempty"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

MLModel is a model a vhost can call: where it is served and how to send it rows. The model itself lives on a model server (hermod-ml, KServe, Triton, MLServer, MLflow); Hermod keeps the address and calls it from the Predict node and the serving endpoints.

func (MLModel) IsRecommender added in v1.18.0

func (m MLModel) IsRecommender() bool

IsRecommender reports whether the model recommends items rather than predicting a column.

func (MLModel) Target added in v1.18.0

func (m MLModel) Target(token string) inference.Target

Target is the call this model makes, with the bearer token supplied by the caller (it is looked up from TokenSecret, never held here).

type MLModelStore added in v1.18.0

type MLModelStore interface {
	// ListMLModels returns the vhost's models ordered by name.
	ListMLModels(ctx context.Context, vhost string) ([]MLModel, error)
	// GetMLModel returns one model, or ErrNotFound.
	GetMLModel(ctx context.Context, vhost, name string) (MLModel, error)
	// PutMLModel creates the model or replaces its definition. It never
	// changes the serving key; SetMLModelServingKey does.
	PutMLModel(ctx context.Context, m MLModel) error
	// SetMLModelServingKey stores the hash of a new serving key, or turns
	// serving off when hash is empty. It returns ErrNotFound for no such model.
	SetMLModelServingKey(ctx context.Context, vhost, name, hash string) error
	// DeleteMLModel removes one model, or returns ErrNotFound.
	DeleteMLModel(ctx context.Context, vhost, name string) error
	// DeleteMLModels removes every model the vhost holds.
	DeleteMLModels(ctx context.Context, vhost string) error
}

MLModelStore is implemented by a storage backend that can hold models per vhost. Like VHostSecretStore it is separate from Storage; callers find it with a type assertion.

type MLMonitoring added in v1.18.0

type MLMonitoring struct {
	// LogSampleRate is the share of predictions logged, 0 to 1.
	LogSampleRate float64 `json:"log_sample_rate,omitempty"`
	// LogMaskFields are input or output fields masked in a logged row, as
	// dotted paths; "*" masks every string.
	LogMaskFields []string `json:"log_mask_fields,omitempty"`
	// LogMaskType is one of MLMaskTypes; empty means "all".
	LogMaskType string `json:"log_mask_type,omitempty"`
	// LogRetention is how long a logged row is kept, as "7d" or "12h"; empty
	// means DefaultMLLogRetention.
	LogRetention string `json:"log_retention,omitempty"`

	// DriftWarn and DriftAlert are the population stability index at which a
	// feature is reported as drifting, and at which an alert is sent; zero
	// means the default.
	DriftWarn  float64 `json:"drift_warn,omitempty"`
	DriftAlert float64 `json:"drift_alert,omitempty"`
}

MLMonitoring is how a model is watched.

Prediction logging is off until LogSampleRate is above zero: a logged row holds what the model was sent, which may be personal data, so it is never kept unless someone asks for it. LogMaskFields are masked before a row is written, never after.

func (MLMonitoring) Logging added in v1.18.0

func (m MLMonitoring) Logging() bool

Logging reports whether any prediction is logged.

func (MLMonitoring) Retention added in v1.18.0

func (m MLMonitoring) Retention() time.Duration

Retention returns how long a logged prediction is kept. An unreadable value, which Validate refuses, is read as the default.

func (MLMonitoring) Thresholds added in v1.18.0

func (m MLMonitoring) Thresholds() (warn, alert float64)

Thresholds returns the drift thresholds, defaults filled in.

func (MLMonitoring) Validate added in v1.18.0

func (m MLMonitoring) Validate() error

Validate reports what is wrong with the setting.

type MLPredictionLog added in v1.18.0

type MLPredictionLog struct {
	VHost     string         `json:"vhost"`
	Model     string         `json:"model"`
	Version   string         `json:"version,omitempty"`
	Timestamp time.Time      `json:"timestamp"`
	Inputs    map[string]any `json:"inputs"`
	Outputs   map[string]any `json:"outputs"`
	// LatencyMs is how long the whole call, of which this row was part, took.
	LatencyMs  float64 `json:"latency_ms"`
	CallerKind string  `json:"caller_kind"`
	CallerID   string  `json:"caller_id,omitempty"`
}

MLPredictionLog is one logged prediction: one row a model was sent and what it answered. Inputs hold the row after masking.

type MLPredictionLogStore added in v1.18.0

type MLPredictionLogStore interface {
	// InsertMLPredictionLogs writes a batch of logged predictions.
	InsertMLPredictionLogs(ctx context.Context, logs []MLPredictionLog) error
	// ListMLPredictionLogs returns a model's most recent logged predictions,
	// newest first, at most limit (bounded by MaxMLPredictionLogPage).
	ListMLPredictionLogs(ctx context.Context, vhost, model string, limit int) ([]MLPredictionLog, error)
	// PurgeMLPredictionLogs removes logged predictions older than before: of
	// one model, of every model of the vhost when model is empty, or of
	// everything when vhost is empty too.
	PurgeMLPredictionLogs(ctx context.Context, vhost, model string, before time.Time) error
	// DeleteMLPredictionLogs removes every logged prediction of a model, or of
	// every model of the vhost when model is empty.
	DeleteMLPredictionLogs(ctx context.Context, vhost, model string) error
}

MLPredictionLogStore is implemented by a log store that can keep a model's logged predictions. Like MLModelStore, callers find it with a type assertion.

type MLQuotaStore added in v1.18.0

type MLQuotaStore interface {
	// GetMLQuotas returns the vhost's quotas, or ErrNotFound when none are set.
	GetMLQuotas(ctx context.Context, vhost string) (MLQuotas, error)
	// PutMLQuotas replaces the vhost's quotas.
	PutMLQuotas(ctx context.Context, q MLQuotas) error
	// DeleteMLQuotas removes the vhost's quotas; none is not an error.
	DeleteMLQuotas(ctx context.Context, vhost string) error
}

MLQuotaStore is implemented by a storage backend that can hold quotas per vhost. Like MLModelStore it is separate from Storage; callers find it with a type assertion.

type MLQuotas added in v1.18.0

type MLQuotas struct {
	VHost string `json:"vhost"`

	// MaxDatasets bounds how many datasets the vhost holds.
	MaxDatasets *int64 `json:"max_datasets"`
	// MaxDatasetRows bounds the rows one dataset holds.
	MaxDatasetRows *int64 `json:"max_dataset_rows"`
	// MaxDatasetBytes bounds what one fill of a dataset sends to the ML
	// worker: the file as uploaded, or the rows of a query as JSON.
	MaxDatasetBytes *int64 `json:"max_dataset_bytes"`
	// MaxModels bounds how many models the vhost registers or trains.
	MaxModels *int64 `json:"max_models"`
	// MaxConcurrentTrainings bounds the vhost's trainings running at once.
	MaxConcurrentTrainings *int64 `json:"max_concurrent_trainings"`
	// MaxPredictionsPerSecond bounds the rows the vhost's models are sent per
	// second.
	MaxPredictionsPerSecond *float64 `json:"max_predictions_per_second"`

	UpdatedBy string    `json:"updated_by,omitempty"`
	UpdatedAt time.Time `json:"updated_at"`
}

MLQuotas are the machine-learning limits one vhost has been given. A nil field is not set for the vhost, so the server-wide default applies (see internal/ml); zero means no limit; a positive value is the limit.

type MLRetrainPolicy added in v1.18.0

type MLRetrainPolicy struct {
	// Schedule is a five-field cron expression, or a descriptor such as
	// "@daily". Empty means no schedule.
	Schedule string `json:"schedule,omitempty"`
	// NewRows trains again once the dataset holds this many rows more than
	// when the model last trained. Zero means no row trigger.
	NewRows int `json:"new_rows,omitempty"`

	Spec   worker.TrainSpec `json:"spec"`
	GoLive MLGoLive         `json:"go_live"`

	UpdatedBy string    `json:"updated_by,omitempty"`
	UpdatedAt time.Time `json:"updated_at"`
}

MLRetrainPolicy is when a model Hermod trained trains again by itself, and how: on a cron schedule, after NewRows rows were added to its dataset since it last trained, or either, whichever comes first.

type MLRetrainStatus added in v1.18.0

type MLRetrainStatus struct {
	// At is when the last retraining ran; zero until one has. Trigger is
	// "schedule" or "new_rows".
	At      time.Time `json:"at"`
	Trigger string    `json:"trigger,omitempty"`
	// Version is the version it made, Live whether that version went live and
	// Reason why, in words. Error is set instead when it failed.
	Version string `json:"version,omitempty"`
	Live    bool   `json:"live"`
	Reason  string `json:"reason,omitempty"`
	Error   string `json:"error,omitempty"`

	// DatasetRows is how many rows the dataset held when the model last
	// trained, by any trigger, and TrainedAt when that was.
	DatasetRows int       `json:"dataset_rows"`
	TrainedAt   time.Time `json:"trained_at"`
}

MLRetrainStatus is what a model's last retraining did, and what its next one counts new rows from.

type MLRetrainStore added in v1.18.0

type MLRetrainStore interface {
	// ListRetrainingMLModels returns every model, of every vhost, that has a
	// retrain policy.
	ListRetrainingMLModels(ctx context.Context) ([]MLModel, error)
	// SetMLModelRetrain stores the model's retrain policy, or clears it when
	// p is nil. It returns ErrNotFound for no such model.
	SetMLModelRetrain(ctx context.Context, vhost, name string, p *MLRetrainPolicy) error
	// SetMLModelRetrainStatus stores what the model's last training did. It
	// returns ErrNotFound for no such model.
	SetMLModelRetrainStatus(ctx context.Context, vhost, name string, st MLRetrainStatus) error
	// ClaimMLModelTraining marks the model as being trained by owner for ttl,
	// and reports whether it could: false while another owner's claim has not
	// expired. The owner may claim again to extend its own. It returns
	// ErrNotFound for no such model.
	ClaimMLModelTraining(ctx context.Context, vhost, name, owner string, ttl time.Duration) (bool, error)
	// ReleaseMLModelTraining ends owner's claim; anyone else's is left alone.
	ReleaseMLModelTraining(ctx context.Context, vhost, name, owner string) error
}

MLRetrainStore is implemented by a storage backend that keeps retrain policies with its models. Like MLModelStore, callers find it with a type assertion.

type MLSchedule added in v1.18.0

type MLSchedule struct {
	VHost    string          `json:"vhost"`
	Kind     string          `json:"kind"`
	Name     string          `json:"name"`
	Schedule string          `json:"schedule"`
	Spec     json.RawMessage `json:"spec"`

	LastRun   time.Time `json:"last_run"`
	LastRows  int       `json:"last_rows"`
	LastError string    `json:"last_error,omitempty"`

	UpdatedBy string    `json:"updated_by,omitempty"`
	UpdatedAt time.Time `json:"updated_at"`
}

MLSchedule is something Hermod does by itself on a cron schedule for one dataset or model of a vhost: a dataset refresh or a model's build. Spec is the kind's own definition (internal/ml reads it); LastRun, LastRows and LastError are what its latest run did.

type MLScheduleStore added in v1.18.0

type MLScheduleStore interface {
	// ListMLSchedules returns every schedule of every vhost.
	ListMLSchedules(ctx context.Context) ([]MLSchedule, error)
	// GetMLSchedule returns one schedule, or ErrNotFound.
	GetMLSchedule(ctx context.Context, vhost, kind, name string) (MLSchedule, error)
	// PutMLSchedule saves a schedule's definition, keeping what its last run
	// did. UpdatedAt is set here.
	PutMLSchedule(ctx context.Context, s MLSchedule) error
	// DeleteMLSchedule removes a schedule, or returns ErrNotFound.
	DeleteMLSchedule(ctx context.Context, vhost, kind, name string) error
	// SetMLScheduleRun records what a run did. A schedule removed meanwhile
	// is not an error.
	SetMLScheduleRun(ctx context.Context, vhost, kind, name string, at time.Time, rows int, runErr string) error
	// ClaimMLSchedule marks the schedule as run by owner for ttl, and
	// reports whether it could: false while another owner's claim has not
	// expired, or for no such schedule. The owner may claim again.
	ClaimMLSchedule(ctx context.Context, vhost, kind, name, owner string, ttl time.Duration) (bool, error)
	// ReleaseMLSchedule ends owner's claim; anyone else's is left alone.
	ReleaseMLSchedule(ctx context.Context, vhost, kind, name, owner string) error
	// DeleteMLSchedules removes every schedule of a vhost.
	DeleteMLSchedules(ctx context.Context, vhost string) error
}

MLScheduleStore is implemented by a storage backend that keeps ML schedules. Like MLModelStore, callers find it with a type assertion.

type MLScoringStore added in v1.18.0

type MLScoringStore interface {
	// SetMLModelScoring stores where the model is scored. It returns
	// ErrNotFound for no such model.
	SetMLModelScoring(ctx context.Context, vhost, name, scoring string) error
}

MLScoringStore is implemented by a storage backend that keeps a trained model's scoring mode with it. Like MLRetrainStore, callers find it with a type assertion.

type MLScript added in v1.18.0

type MLScript struct {
	VHost   string `json:"vhost"`
	Name    string `json:"name"`
	Version int    `json:"version"`
	SHA256  string `json:"sha256"`
	// Source is empty in listings; GetMLScript fills it.
	Source      string    `json:"source,omitempty"`
	Description string    `json:"description,omitempty"`
	CreatedBy   string    `json:"created_by,omitempty"`
	CreatedAt   time.Time `json:"created_at"`
}

MLScript is one version of a custom training script a vhost holds: Python that trains a model and exports it to ONNX, run by a sandboxed hermod-ml worker pool. A script is versioned by the SHA-256 of its source. Saving new source under a name adds a version; saving the source the latest version already has adds nothing. Versions are never rewritten, so a model version can always say exactly which code trained it.

type MLScriptStore added in v1.18.0

type MLScriptStore interface {
	// ListMLScripts returns the latest version of each of the vhost's
	// scripts, ordered by name, without their source.
	ListMLScripts(ctx context.Context, vhost string) ([]MLScript, error)
	// ListMLScriptVersions returns every version of one script, newest
	// first, without their source.
	ListMLScriptVersions(ctx context.Context, vhost, name string) ([]MLScript, error)
	// GetMLScript returns the latest version of a script with its source, or
	// ErrNotFound.
	GetMLScript(ctx context.Context, vhost, name string) (MLScript, error)
	// PutMLScript saves s.Source as the script's next version, unless it is
	// the source of the latest version already, and returns the version that
	// holds it. Version, SHA256 and CreatedAt are set here.
	PutMLScript(ctx context.Context, s MLScript) (MLScript, error)
	// DeleteMLScript removes every version of a script, or returns ErrNotFound.
	DeleteMLScript(ctx context.Context, vhost, name string) error
	// DeleteMLScripts removes every script the vhost holds.
	DeleteMLScripts(ctx context.Context, vhost string) error
}

MLScriptStore is implemented by a storage backend that can hold custom training scripts per vhost. Like MLModelStore it is separate from Storage; callers find it with a type assertion.

type MessageTrace

type MessageTrace struct {
	ID         string             `json:"id"`
	MessageID  string             `json:"message_id"`
	WorkflowID string             `json:"workflow_id"`
	Steps      []hermod.TraceStep `json:"steps"`
	CreatedAt  time.Time          `json:"created_at"`

	// Summary fields, carried on the parent row so the trace list never has to
	// touch the step table. Listing used to aggregate every step a workflow had
	// ever produced — measured on PostgreSQL 17, a sequential scan of 250k rows
	// and ~390 MB of I/O to return 25 of them — because "when did this message
	// start" was only derivable as MIN(timestamp) over its steps. It is now
	// written once, when the steps are.
	StepCount  int   `json:"step_count"`
	ErrorCount int   `json:"error_count"`
	DurationMs int64 `json:"duration_ms"`
}

type MissingRef added in v1.8.1

type MissingRef struct {
	Kind   string `json:"kind"` // "source" or "sink"
	ID     string `json:"id"`
	NodeID string `json:"node_id,omitempty"`
}

MissingRef is one dependency an export could not resolve.

type OutboxItem

type OutboxItem = hermod.OutboxItem

type Plugin

type Plugin struct {
	ID          string     `json:"id"`
	Name        string     `json:"name"`
	Description string     `json:"description"`
	Author      string     `json:"author"`
	Stars       int        `json:"stars"`
	Category    string     `json:"category"`
	Certified   bool       `json:"certified"`
	Type        string     `json:"type"` // WASM, Connector, Transformer
	WasmURL     string     `json:"wasm_url,omitempty"`
	Installed   bool       `json:"installed"`
	InstalledAt *time.Time `json:"installed_at" omitzero:"true"`
	// CreatedAt is the sort key the plugin list pages on. Storage stamps it when a
	// caller leaves it zero and never rewrites it on update. See Workflow.CreatedAt.
	CreatedAt time.Time `json:"created_at" bson:"created_at"`
}

type Role

type Role string
const (
	RoleAdministrator Role = "Administrator"
	RoleEditor        Role = "Editor"
	RoleViewer        Role = "Viewer"
)

type Schema

type Schema struct {
	ID        string    `json:"id"`
	Name      string    `json:"name"`
	Version   int       `json:"version"`
	Type      string    `json:"type"`
	Content   string    `json:"content"`
	CreatedAt time.Time `json:"created_at" omitzero:"true"`
}

type Sink

type Sink struct {
	ID          string           `json:"id"`
	Name        string           `json:"name"`
	Type        string           `json:"type"`
	VHost       string           `json:"vhost"`
	Active      bool             `json:"active"`
	Status      string           `json:"status,omitempty"`
	WorkerID    string           `json:"worker_id"`
	WorkspaceID string           `json:"workspace_id,omitempty"`
	Config      hermod.StringMap `json:"config"`
	// CreatedAt is the sort key the sink list pages on. Storage stamps it
	// when a caller leaves it zero and never rewrites it on update, so it stays
	// the creation time rather than the last-touched time. See Workflow.CreatedAt.
	CreatedAt time.Time `json:"created_at" bson:"created_at"`
}

type Source

type Source struct {
	ID          string            `json:"id"`
	Name        string            `json:"name"`
	Type        string            `json:"type"`
	VHost       string            `json:"vhost"`
	Active      bool              `json:"active"`
	Status      string            `json:"status,omitempty"`
	WorkerID    string            `json:"worker_id"`
	WorkspaceID string            `json:"workspace_id,omitempty"`
	Config      hermod.StringMap  `json:"config"`
	Sample      string            `json:"sample,omitempty"`
	State       map[string]string `json:"state" omitzero:"true"`
	// CreatedAt is the sort key the source list pages on. Storage stamps it
	// when a caller leaves it zero and never rewrites it on update, so it stays
	// the creation time rather than the last-touched time. See Workflow.CreatedAt.
	CreatedAt time.Time `json:"created_at" bson:"created_at"`
}

type Storage

type Storage interface {
	// Init performs storage initialization/migrations and is safe to call multiple times.
	Init(ctx context.Context) error

	// Ping checks the connection to the storage.
	Ping(ctx context.Context) error

	ListSources(ctx context.Context, filter CommonFilter) ([]Source, int, error)
	CreateSource(ctx context.Context, src Source) error
	UpdateSource(ctx context.Context, src Source) error
	UpdateSourceStatus(ctx context.Context, id string, status string) error
	UpdateSourceState(ctx context.Context, id string, state map[string]string) error
	// UpdateSourceSample writes only the stored preview payload. Storing a
	// sample through UpdateSource carried every other column along with it,
	// from whatever copy of the source the caller happened to hold.
	UpdateSourceSample(ctx context.Context, id string, sample string) error
	DeleteSource(ctx context.Context, id string) error
	GetSource(ctx context.Context, id string) (Source, error)

	ListSinks(ctx context.Context, filter CommonFilter) ([]Sink, int, error)
	CreateSink(ctx context.Context, snk Sink) error
	UpdateSink(ctx context.Context, snk Sink) error

	// ReEncryptSecrets rewrites every stored credential under newKey. Key
	// rotation must call this *before* installing the new key: rotating
	// without it leaves every password and API key encrypted under a key the
	// process no longer has.
	ReEncryptSecrets(ctx context.Context, newKey string) error
	UpdateSinkStatus(ctx context.Context, id string, status string) error
	DeleteSink(ctx context.Context, id string) error
	GetSink(ctx context.Context, id string) (Sink, error)

	ListUsers(ctx context.Context, filter CommonFilter) ([]User, int, error)
	CreateUser(ctx context.Context, user User) error
	UpdateUser(ctx context.Context, user User) error
	DeleteUser(ctx context.Context, id string) error
	GetUser(ctx context.Context, id string) (User, error)
	GetUserByUsername(ctx context.Context, username string) (User, error)
	GetUserByEmail(ctx context.Context, email string) (User, error)

	ListVHosts(ctx context.Context, filter CommonFilter) ([]VHost, int, error)
	CreateVHost(ctx context.Context, vhost VHost) error
	UpdateVHost(ctx context.Context, vhost VHost) error
	DeleteVHost(ctx context.Context, id string) error
	GetVHost(ctx context.Context, id string) (VHost, error)

	ListWorkflows(ctx context.Context, filter CommonFilter) ([]Workflow, int, error)
	ListWorkspaces(ctx context.Context) ([]Workspace, error)
	CreateWorkspace(ctx context.Context, ws Workspace) error
	UpdateWorkspace(ctx context.Context, ws Workspace) error
	GetWorkspace(ctx context.Context, id string) (Workspace, error)
	DeleteWorkspace(ctx context.Context, id string) error
	// ClearWorkspaceAssignments removes every workflow, source and sink from
	// the given workspace and reports how many rows it touched. Deleting a
	// workspace without this leaves members pointing at an id that no longer
	// resolves: the list renders a raw UUID, no filter matches them, and the
	// quota checks stop applying because GetWorkspace errors and they bail.
	ClearWorkspaceAssignments(ctx context.Context, workspaceID string) (int, error)
	CreateWorkflow(ctx context.Context, wf Workflow) error
	UpdateWorkflow(ctx context.Context, wf Workflow) error
	UpdateWorkflowStatus(ctx context.Context, id string, status string) error
	UpdateWorkflowStats(ctx context.Context, id string, processed, errors, lag uint64) error
	DeleteWorkflow(ctx context.Context, id string) error
	GetWorkflow(ctx context.Context, id string) (Workflow, error)

	// Lease-based ownership for workflows
	// AcquireWorkflowLease attempts to set owner_id and extend lease_until atomically with TTL seconds.
	// Returns true if the lease was acquired.
	AcquireWorkflowLease(ctx context.Context, workflowID, ownerID string, ttlSeconds int) (bool, error)
	// RenewWorkflowLease extends lease_until for an existing owner if not expired yet. Returns true if renewed.
	RenewWorkflowLease(ctx context.Context, workflowID, ownerID string, ttlSeconds int) (bool, error)
	// ReleaseWorkflowLease clears ownership if owned by ownerID.
	ReleaseWorkflowLease(ctx context.Context, workflowID, ownerID string) error

	ListWorkers(ctx context.Context, filter CommonFilter) ([]Worker, int, error)
	CreateWorker(ctx context.Context, worker Worker) error
	UpdateWorker(ctx context.Context, worker Worker) error
	UpdateWorkerHeartbeat(ctx context.Context, id string, res WorkerResources) error
	DeleteWorker(ctx context.Context, id string) error
	GetWorker(ctx context.Context, id string) (Worker, error)

	ListLogs(ctx context.Context, filter LogFilter) ([]Log, int, error)
	CreateLog(ctx context.Context, log Log) error
	CreateLogs(ctx context.Context, logs []Log) error
	DeleteLogs(ctx context.Context, filter LogFilter) error
	PurgeLogs(ctx context.Context, before time.Time) error

	ListAuditLogs(ctx context.Context, filter AuditFilter) ([]AuditLog, int, error)
	CreateAuditLog(ctx context.Context, log AuditLog) error
	PurgeAuditLogs(ctx context.Context, before time.Time) error
	PurgeMessageTraces(ctx context.Context, retention TraceRetention) error
	DeleteWorkflowMessageTraces(ctx context.Context, workflowID string) error

	ListWebhookRequests(ctx context.Context, filter WebhookRequestFilter) ([]WebhookRequest, int, error)
	CreateWebhookRequest(ctx context.Context, req WebhookRequest) error
	GetWebhookRequest(ctx context.Context, id string) (WebhookRequest, error)
	DeleteWebhookRequests(ctx context.Context, filter WebhookRequestFilter) error

	CreateFormSubmission(ctx context.Context, sub FormSubmission) error
	ListFormSubmissions(ctx context.Context, filter FormSubmissionFilter) ([]FormSubmission, int, error)
	GetFormSubmission(ctx context.Context, id string) (FormSubmission, error)
	UpdateFormSubmissionStatus(ctx context.Context, id string, status string) error
	DeleteFormSubmissions(ctx context.Context, filter FormSubmissionFilter) error

	GetSetting(ctx context.Context, key string) (string, error)
	SaveSetting(ctx context.Context, key string, value string) error

	// Node State Management
	UpdateNodeState(ctx context.Context, workflowID, nodeID string, state any) error
	GetNodeStates(ctx context.Context, workflowID string) (map[string]any, error)

	// Schema Registry
	ListSchemas(ctx context.Context, name string) ([]Schema, error)
	ListAllSchemas(ctx context.Context) ([]Schema, error)
	GetSchema(ctx context.Context, name string, version int) (Schema, error)
	GetLatestSchema(ctx context.Context, name string) (Schema, error)
	CreateSchema(ctx context.Context, schema Schema) error

	// Message Tracing
	RecordTraceStep(ctx context.Context, workflowID, messageID string, step hermod.TraceStep) error
	GetMessageTrace(ctx context.Context, workflowID, messageID string) (MessageTrace, error)
	ListMessageTraces(ctx context.Context, workflowID string, filter TraceFilter) ([]MessageTrace, error)

	// Workflow Versioning
	CreateWorkflowVersion(ctx context.Context, version WorkflowVersion) error
	ListWorkflowVersions(ctx context.Context, workflowID string) ([]WorkflowVersion, error)
	GetWorkflowVersion(ctx context.Context, workflowID string, version int) (WorkflowVersion, error)

	// Transactional Outbox
	CreateOutboxItem(ctx context.Context, item OutboxItem) error
	ListOutboxItems(ctx context.Context, status string, limit int) ([]OutboxItem, error)
	DeleteOutboxItem(ctx context.Context, id string) error
	UpdateOutboxItem(ctx context.Context, item OutboxItem) error

	// Lineage
	GetLineage(ctx context.Context) ([]LineageEdge, error)

	// Marketplace
	ListPlugins(ctx context.Context) ([]Plugin, error)
	GetPlugin(ctx context.Context, id string) (Plugin, error)
	InstallPlugin(ctx context.Context, id string) error
	UninstallPlugin(ctx context.Context, id string) error

	// Approvals
	ListApprovals(ctx context.Context, filter ApprovalFilter) ([]Approval, int, error)
	CreateApproval(ctx context.Context, app Approval) error
	GetApproval(ctx context.Context, id string) (Approval, error)
	UpdateApprovalStatus(ctx context.Context, id string, status string, processedBy string, notes string, formData map[string]any) error
	DeleteApproval(ctx context.Context, id string) error

	// Suspended Messages
	CreateSuspendedMessage(ctx context.Context, m SuspendedMessage) error
	ListSuspendedMessages(ctx context.Context, workflowID string, before time.Time) ([]SuspendedMessage, error)
	DeleteSuspendedMessage(ctx context.Context, id string) error

	// Aggregated Dashboard Stats
	GetDashboardStats(ctx context.Context, vhost string) (DashboardStats, error)

	// Dashboard history. Samples are appended on an interval so the dashboard
	// chart survives a page reload; PurgeDashboardHistory bounds the table.
	RecordDashboardSample(ctx context.Context, sample DashboardSample) error
	GetDashboardHistory(ctx context.Context, vhost string, since time.Time, limit int) ([]DashboardSample, error)
	PurgeDashboardHistory(ctx context.Context, before time.Time) error
}

type StoredStep added in v1.10.0

type StoredStep struct {
	NodeID    string        `bson:"node_id" json:"node_id"`
	Timestamp time.Time     `bson:"timestamp" json:"timestamp"`
	Duration  time.Duration `bson:"duration,omitempty" json:"duration,omitempty"`
	AfterKey  string        `bson:"after_key,omitempty" json:"after_key,omitempty"`
	Error     string        `bson:"error,omitempty" json:"error,omitempty"`
	Lineage   string        `bson:"lineage,omitempty" json:"lineage,omitempty"`

	// Documents written before payload dedup carry the maps inline, under the
	// field names the driver derived from hermod.TraceStep's own fields. They
	// are read and never written, so an upgrade needs no backfill.
	LegacyBefore map[string]any `bson:"before,omitempty" json:"before,omitempty"`
	LegacyAfter  map[string]any `bson:"after,omitempty" json:"after,omitempty"`
}

StoredStep is one node's step. It names its payload instead of carrying it.

func PackTraceStep added in v1.10.0

func PackTraceStep(step hermod.TraceStep) (StoredStep, string, []byte)

PackTraceStep turns a step into its stored form plus the payload to file under the returned key. An empty key means the step carried no payload.

type StoredTrace added in v1.10.0

type StoredTrace struct {
	ID         string    `bson:"id" json:"id"`
	WorkflowID string    `bson:"workflow_id" json:"workflow_id"`
	MessageID  string    `bson:"message_id" json:"message_id"`
	CreatedAt  time.Time `bson:"created_at" json:"created_at"`

	Steps []StoredStep `bson:"steps" json:"steps"`

	// Payloads maps a content key to the encoded payload that produced it.
	// Compressed, and self-describing about how — see EncodeTracePayload.
	Payloads map[string][]byte `bson:"payloads,omitempty" json:"payloads,omitempty"`
}

StoredTrace is one trace as written to a document store.

func (StoredTrace) ErrorCount added in v1.10.0

func (t StoredTrace) ErrorCount() int

ErrorCount is how many steps recorded an error, without decoding a payload.

The trace list needs it and nothing else about the steps; decompressing every payload of every trace to count errors would be the whole-table read that the SQL backends added a parent row to avoid.

func (StoredTrace) UnpackTrace added in v1.10.0

func (t StoredTrace) UnpackTrace() MessageTrace

UnpackTrace rebuilds the trace a caller sees from the document that was stored.

type SuspendedMessage

type SuspendedMessage struct {
	ID         string            `json:"id"`
	WorkflowID string            `json:"workflow_id"`
	NodeID     string            `json:"node_id"`
	Payload    []byte            `json:"payload"`
	Metadata   map[string]string `json:"metadata"`
	Data       map[string]any    `json:"data"`
	ResumeAt   time.Time         `json:"resume_at"`
	CreatedAt  time.Time         `json:"created_at"`
}

type TraceDedupCache added in v1.10.0

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

traceDedupCache remembers which payloads already have their bytes in the table, so the steps that follow can store a reference instead of a copy.

It is deliberately a cache and not an index, and it is consulted pessimistically. An entry is added only *after* an insert has actually succeeded, and "not in the cache" means "store the bytes". So a cold start, a second control-plane replica, an eviction under load or a race between two steps of the same message all cost a duplicate copy of a payload — never a reference with nothing behind it. Getting that direction the wrong way round would turn a full table into a blank trace viewer.

func NewTraceDedupCache added in v1.10.0

func NewTraceDedupCache(capacity int) *TraceDedupCache

func (*TraceDedupCache) AlreadyStored added in v1.10.0

func (c *TraceDedupCache) AlreadyStored(workflowID, messageID string, hash []byte) bool

func (*TraceDedupCache) Len added in v1.10.0

func (c *TraceDedupCache) Len() int

func (*TraceDedupCache) MarkStored added in v1.10.0

func (c *TraceDedupCache) MarkStored(workflowID, messageID string, hash []byte)

type TraceFilter added in v1.8.1

type TraceFilter struct {
	Before time.Time
	Limit  int
	Offset int
}

TraceFilter selects a page of message traces.

Before is a keyset cursor: pass the CreatedAt of the last row you saw and the next page is an index seek, not a scan-and-discard. Offset remains for callers that page by number, and is cheap now that it walks one row per message rather than one per step — but it still reads and throws away everything it skips, so Before is the one to reach for.

type TraceRetention added in v1.10.0

type TraceRetention struct {
	// Keep maps a workflow id to the oldest trace step it keeps.
	//
	// A workflow with no entry here keeps everything it has. Either no window
	// is configured or the configured one could not be parsed, and neither is
	// a licence to delete: an operator-supplied window has no safe default,
	// because a short one deletes traces nobody asked to lose and none at all
	// restores the growth bug this mechanism exists to prevent.
	Keep map[string]time.Time

	// Live is every workflow that exists, including those absent from Keep.
	//
	// Traces of a workflow that is not here are unreachable — the viewer
	// reaches a trace through its workflow, and ListMessageTraces takes a
	// workflow id — so they are swept regardless of age. Nothing else removes
	// them: DeleteWorkflow deletes one row.
	Live map[string]struct{}

	// LiveIsComplete reports whether Live really is every workflow there is.
	//
	// Not inferred from Live being non-empty, and not a detail. The caller
	// reads workflows through a paged list; on a deployment with more
	// workflows than it asked for, every workflow past the first page would
	// look like a deleted one and have its traces swept. When this is false
	// the orphan sweep is skipped and everything else still runs.
	LiveIsComplete bool
}

TraceRetention describes one sweep of the message trace tables.

A cutoff *per workflow*, where this used to be a single time.Time. Retention is a per-workflow setting, but the purge it drove was `DELETE FROM message_trace_steps WHERE timestamp < ?` with no workflow predicate, called from inside a loop over workflows. So the shortest window in the deployment decided what every workflow kept: one set to 7d deleted the traces of one set to 365d, and on PostgreSQL it dropped whole day partitions out from under it, which no VACUUM undoes. That same full-table delete also ran once per workflow per hour.

func (TraceRetention) Orphans added in v1.10.0

func (r TraceRetention) Orphans(present []string) ([]string, bool)

Orphans returns the workflow ids in present that no longer exist, and whether the caller is entitled to act on the answer.

func (TraceRetention) PartitionFloor added in v1.10.0

func (r TraceRetention) PartitionFloor() (time.Time, bool)

PartitionFloor is the oldest moment no live workflow needs any more, and whether one exists at all.

Only whole days below this may be reclaimed by dropping a partition, because a drop takes every workflow's rows for that day at once. A single live workflow that keeps everything therefore blocks partition dropping for the whole deployment — correctly: the alternative is destroying its traces to save space on someone else's.

type TraceStep

type TraceStep = hermod.TraceStep

type Transformation

type Transformation struct {
	Type   string         `json:"type"`
	Config map[string]any `json:"config"`
}

type UnitTest

type UnitTest struct {
	Name           string         `json:"name"`
	Input          map[string]any `json:"input"`
	ExpectedOutput map[string]any `json:"expected_output"`
	Description    string         `json:"description,omitempty"`
}

type User

type User struct {
	ID               string   `json:"id"`
	Username         string   `json:"username"`
	Password         string   `json:"password,omitempty"`
	FullName         string   `json:"full_name"`
	Email            string   `json:"email"`
	Role             Role     `json:"role"`
	VHosts           []string `json:"vhosts"`
	TwoFactorEnabled bool     `json:"two_factor_enabled"`
	TwoFactorSecret  string   `json:"two_factor_secret,omitempty"`
	// CreatedAt is the sort key the user list pages on. Storage stamps it when a
	// caller leaves it zero and never rewrites it on update. See Workflow.CreatedAt.
	CreatedAt time.Time `json:"created_at" bson:"created_at"`
}

type VHost

type VHost struct {
	ID          string `json:"id"`
	Name        string `json:"name"`
	Description string `json:"description"`
	// CreatedAt is the sort key the vhost list pages on. Storage stamps it when a
	// caller leaves it zero and never rewrites it on update. See Workflow.CreatedAt.
	CreatedAt time.Time `json:"created_at" bson:"created_at"`
}

type VHostSecret added in v1.17.0

type VHostSecret struct {
	VHost     string    `json:"vhost"`
	Name      string    `json:"name"`
	Value     string    `json:"-"`
	UpdatedBy string    `json:"updated_by"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

VHostSecret is one secret a vhost holds: saved from the UI, encrypted at rest, and readable only by that vhost's workflows.

Value is the plaintext while it is in memory. It carries `json:"-"` so that no handler can return it by encoding the struct: the API lists names, and a value leaves storage only to be used.

type VHostSecretStore added in v1.17.0

type VHostSecretStore interface {
	// ListVHostSecrets returns the vhost's secrets ordered by name, without
	// their values.
	ListVHostSecrets(ctx context.Context, vhost string) ([]VHostSecret, error)
	// GetVHostSecret returns one secret with its value, or ErrNotFound.
	GetVHostSecret(ctx context.Context, vhost, name string) (VHostSecret, error)
	// PutVHostSecret creates the secret, or replaces its value if the vhost
	// already holds the name.
	PutVHostSecret(ctx context.Context, secret VHostSecret) error
	// DeleteVHostSecret removes one secret, or returns ErrNotFound.
	DeleteVHostSecret(ctx context.Context, vhost, name string) error
	// DeleteVHostSecrets removes every secret the vhost holds.
	DeleteVHostSecrets(ctx context.Context, vhost string) error
}

VHostSecretStore is implemented by a storage backend that can hold secrets per vhost. It is separate from Storage so that a backend without vhosts does not have to pretend; callers find it with a type assertion.

type WebhookRequest

type WebhookRequest struct {
	ID        string            `json:"id"`
	Timestamp time.Time         `json:"timestamp" omitzero:"true"`
	Path      string            `json:"path"`
	Method    string            `json:"method"`
	Headers   map[string]string `json:"headers"`
	Body      []byte            `json:"body"`
}

type WebhookRequestFilter

type WebhookRequestFilter struct {
	CommonFilter
	Path string `json:"path"`
}

type Worker

type Worker struct {
	ID          string     `json:"id"`
	Name        string     `json:"name" bson:"name"`
	Host        string     `json:"host" bson:"host"`
	Port        int        `json:"port" bson:"port"`
	Description string     `json:"description" bson:"description"`
	Token       string     `json:"token" bson:"token"`
	LastSeen    *time.Time `json:"last_seen" omitzero:"true" bson:"last_seen"`

	// Resources are inlined rather than nested so the JSON and BSON shapes are
	// unchanged for the two fields that were already here.
	//
	// The bson tags on the fields above are load-bearing and were missing. The
	// driver's default is the lowercased Go name, so this struct decoded
	// "lastseen", "cpuusage" and "memoryusage" while UpdateWorkerHeartbeat wrote
	// "last_seen", "cpu_usage" and "memory_usage" — meaning that on the MongoDB
	// backend every worker read back with a nil LastSeen and no usage at all,
	// which the UI renders as permanently Offline.
	WorkerResources `bson:",inline"`

	// Draining is a transient flag (never persisted) used to signal a running
	// worker that the platform has requested a graceful shutdown. It is set on
	// API responses by the platform when an administrator triggers a shutdown.
	Draining bool `json:"draining,omitempty" bson:"-"`
	// CreatedAt is the sort key the worker list pages on. Storage stamps it when a
	// caller leaves it zero and never rewrites it on update. See Workflow.CreatedAt.
	CreatedAt time.Time `json:"created_at" bson:"created_at"`
}

type WorkerResources added in v1.12.0

type WorkerResources struct {
	// CPUUsage and MemoryUsage are fractions in 0..1, not percentages. They
	// predate the rest of this struct and keep their column and JSON names.
	CPUUsage    float64 `json:"cpu_usage,omitempty" bson:"cpu_usage,omitempty"`
	MemoryUsage float64 `json:"memory_usage,omitempty" bson:"memory_usage,omitempty"`

	// CPUCores is the number of logical CPUs visible to the worker process.
	CPUCores int `json:"cpu_cores,omitempty" bson:"cpu_cores,omitempty"`

	// MemoryTotalBytes and MemoryUsedBytes describe physical memory.
	MemoryTotalBytes int64 `json:"memory_total_bytes,omitempty" bson:"memory_total_bytes,omitempty"`
	MemoryUsedBytes  int64 `json:"memory_used_bytes,omitempty" bson:"memory_used_bytes,omitempty"`

	// StorageTotalBytes and StorageUsedBytes describe the filesystem holding
	// the worker's data directory — where SQLite metadata, WAL files and trace
	// payloads are written — rather than every disk on the box. That is the one
	// a full disk stops Hermod on, and the one an operator can act on.
	StorageTotalBytes int64 `json:"storage_total_bytes,omitempty" bson:"storage_total_bytes,omitempty"`
	StorageUsedBytes  int64 `json:"storage_used_bytes,omitempty" bson:"storage_used_bytes,omitempty"`
}

WorkerResources is what one worker reports about the machine it runs on: how much of it there is, and how much of it is gone.

Capacity and utilisation travel together deliberately. A usage fraction on its own does not say how much work a worker can still take — 80% of two cores and 80% of sixty-four are the same number and not the same machine — and until this struct existed the platform stored only the fractions. The scheduler in internal/engine/worker/sharding.go picks between workers on exactly this basis, and the workers page and dashboard report it.

Sizes are bytes because bytes are what the operating system reports; converting to gigabytes is a presentation decision and belongs in the one place that presents it. They are signed int64 rather than uint64 because BSON has no unsigned 64-bit type and the SQL drivers reject one outright — the same reason dashboardSampleDoc gives — and no host has eight exabytes of RAM.

Every field is optional and zero means "not reported", not "none". A worker running a release from before capacity reporting existed sends nothing here, and a cluster total that treated its silence as a machine with no CPU would read worse the more old workers were still running. Readers must skip a worker whose capacity is zero rather than average it in.

func (WorkerResources) Capacity added in v1.12.0

func (r WorkerResources) Capacity() WorkerResources

Capacity is this reading with the load fractions dropped: what the machine is, without a claim about how busy it is.

Registration reports capacity only, and deliberately. The two fractions are scheduler inputs — calculateWeight scores peers on them and admission control sheds load on them — and a worker's own view of itself comes from its local reading, which is empty until the first health check. Persisting a real load figure at registration therefore makes every worker see itself as idle and its peers as loaded, and each one grabs everything it can. Capacity has no such reader: nothing schedules on cores or bytes, and the dashboard wants them immediately.

func (WorkerResources) ReportsCapacity added in v1.12.0

func (r WorkerResources) ReportsCapacity() bool

ReportsCapacity says whether this worker told us how big it is.

Aggregates use it to skip the workers that did not, so an old worker abstains from the cluster totals instead of voting zero into them.

func (WorkerResources) StorageUsage added in v1.12.0

func (r WorkerResources) StorageUsage() float64

StorageUsage is the used share of the data filesystem, 0..1.

Derived rather than stored: CPUUsage and MemoryUsage are persisted because they existed before the byte counts did and the scheduler reads them, but a third fraction alongside the two numbers it is computed from would be the same figure under two names.

type Workflow

type Workflow struct {
	ID                string         `json:"id"`
	Name              string         `json:"name"`
	VHost             string         `json:"vhost"`
	Active            bool           `json:"active"`
	Status            string         `json:"status,omitempty"`
	WorkerID          string         `json:"worker_id"`
	OwnerID           string         `json:"owner_id,omitempty"`
	LeaseUntil        *time.Time     `json:"lease_until" omitzero:"true"`
	Nodes             []WorkflowNode `json:"nodes"`
	Edges             []WorkflowEdge `json:"edges"`
	DeadLetterSinkID  string         `json:"dead_letter_sink_id,omitempty"`
	PrioritizeDLQ     bool           `json:"prioritize_dlq,omitempty"`
	MaxRetries        int            `json:"max_retries,omitempty"`
	RetryInterval     string         `json:"retry_interval,omitempty"`
	ReconnectInterval string         `json:"reconnect_interval,omitempty"`
	DryRun            bool           `json:"dry_run,omitempty"`
	IdleTimeout       string         `json:"idle_timeout,omitempty"` // e.g. "5m"
	Tier              WorkflowTier   `json:"tier,omitempty"`
	// RetentionDays optionally overrides global log retention for this workflow.
	// nil means inherit the global setting; 0 means keep forever; >0 means days to retain.
	RetentionDays     *int     `json:"retention_days,omitempty"`
	SchemaType        string   `json:"schema_type,omitempty"`
	Schema            string   `json:"schema,omitempty"`
	Cron              string   `json:"cron,omitempty"`
	DLQThreshold      int      `json:"dlq_threshold,omitempty"`
	TraceSampleRate   float64  `json:"trace_sample_rate,omitempty"`
	Tags              []string `json:"tags" omitzero:"true"`
	TraceRetention    string   `json:"trace_retention,omitempty"` // e.g. "7d", "30d"
	AuditRetention    string   `json:"audit_retention,omitempty"` // e.g. "30d", "365d"
	WorkspaceID       string   `json:"workspace_id,omitempty"`
	CPURequest        float64  `json:"cpu_request,omitempty"`
	MemoryRequest     float64  `json:"memory_request,omitempty"`
	ThroughputRequest int      `json:"throughput_request,omitempty"`
	TotalProcessed    uint64   `json:"total_processed,omitempty"`
	TotalErrors       uint64   `json:"total_errors,omitempty"`
	TotalLag          uint64   `json:"total_lag,omitempty"`
	// CreatedAt is the sort key the workflow list pages on. Storage stamps it
	// when a caller leaves it zero and never rewrites it on update, so it stays
	// the creation time rather than the last-touched time. The bson tag is here
	// because the mongo backend writes snake_case keys explicitly.
	CreatedAt time.Time `json:"created_at" bson:"created_at"`
}

type WorkflowEdge

type WorkflowEdge struct {
	ID           string         `json:"id"`
	SourceID     string         `json:"source_id"`
	TargetID     string         `json:"target_id"`
	SourceHandle string         `json:"source_handle,omitempty"`
	TargetHandle string         `json:"target_handle,omitempty"`
	Config       map[string]any `json:"config" omitzero:"true"`
}

type WorkflowExportBundle

type WorkflowExportBundle struct {
	Workflow Workflow `json:"workflow"`
	Sources  []Source `json:"sources,omitempty" omitzero:"true"`
	Sinks    []Sink   `json:"sinks,omitempty" omitzero:"true"`

	// MissingRefs names the dependencies the workflow points at that no longer
	// exist on the exporting instance. They were previously dropped from the
	// bundle in silence, so an export of a workflow with a dangling reference
	// looked complete and imported into a workflow that could not start.
	MissingRefs []MissingRef `json:"missing_refs,omitempty" omitzero:"true"`
}

WorkflowExportBundle packages a workflow along with its referenced dependencies (like sources and sinks) for easy export and import between Hermod instances.

type WorkflowHealth

type WorkflowHealth struct {
	WorkflowID string        `json:"workflow_id"`
	Status     string        `json:"status"` // "healthy", "degraded", "error"
	Issues     []string      `json:"issues" omitzero:"true"`
	Uptime     time.Duration `json:"uptime"`
	Processed  uint64        `json:"processed"`
	Errors     uint64        `json:"errors"`
	Lag        uint64        `json:"lag"`
	MPS        float64       `json:"mps"`
	Latency    time.Duration `json:"latency"`
}

type WorkflowNode

type WorkflowNode struct {
	ID        string         `json:"id"`
	Type      string         `json:"type"`             // source, sink, transformer, condition, etc.
	RefID     string         `json:"ref_id,omitempty"` // ID of the source, sink, or transformation
	Config    map[string]any `json:"config" omitzero:"true"`
	X         float64        `json:"x"`
	Y         float64        `json:"y"`
	UnitTests []UnitTest     `json:"unit_tests" omitzero:"true"`
}

type WorkflowTier

type WorkflowTier string
const (
	WorkflowTierHot  WorkflowTier = "Hot"
	WorkflowTierCold WorkflowTier = "Cold"
)

type WorkflowVersion

type WorkflowVersion struct {
	ID             string         `json:"id"`
	WorkflowID     string         `json:"workflow_id"`
	Version        int            `json:"version"`
	Nodes          []WorkflowNode `json:"nodes"`
	Edges          []WorkflowEdge `json:"edges"`
	TraceRetention string         `json:"trace_retention,omitempty"`
	AuditRetention string         `json:"audit_retention,omitempty"`
	Config         string         `json:"config"` // JSON of other workflow settings
	CreatedAt      time.Time      `json:"created_at"`
	CreatedBy      string         `json:"created_by"`
	Message        string         `json:"message"` // commit message for the version
}

type Workspace

type Workspace struct {
	ID            string    `json:"id"`
	Name          string    `json:"name"`
	Description   string    `json:"description"`
	MaxWorkflows  int       `json:"max_workflows"`
	MaxCPU        float64   `json:"max_cpu"`
	MaxMemory     float64   `json:"max_memory"`
	MaxThroughput int       `json:"max_throughput"` // messages per second
	CreatedAt     time.Time `json:"created_at"`
}

Directories

Path Synopsis
Package configsecrets decides which connector configuration values are credentials, and encrypts them at rest.
Package configsecrets decides which connector configuration values are credentials, and encrypts them at rest.
Package mlblobtest is the contract every storage.MLBlobStore keeps, run against each backend by its own tests.
Package mlblobtest is the contract every storage.MLBlobStore keeps, run against each backend by its own tests.

Jump to

Keyboard shortcuts

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