Documentation
¶
Overview ¶
Package console serves an embedded, read-only, single-operator web UI over a bintrail index. It is the MCP server with a web face: the same query, recovery, status, and metadata engines, reached over HTTP from a browser.
The console serves event browsing, recovery-SQL generation, status, and point-in-time reconstruction. The events API includes connection_id but never query_text/query_hash (see dto.go). No endpoint ever executes SQL; recover generates a script for the operator to review and apply by hand.
Index ¶
- Constants
- Variables
- func BackupCutoverAge(interval time.Duration) time.Duration
- func BackupSettingKeys() []string
- func BackupWhyCode(why string) string
- func BaselineFoldSource(e ServerEntry) string
- func CheckBackupSchedule(e ServerEntry, sched BackupSchedule, gates BackupScheduleGates) error
- func CheckFullCopy(e ServerEntry, sched BackupSchedule, gates BackupScheduleGates) error
- func ChooseBackupMethod(ctx context.Context, e ServerEntry, gates BackupScheduleGates) (method, why string, err error)
- func ChooseBackupMethodAt(ctx context.Context, e ServerEntry, gates BackupScheduleGates, now time.Time) (method, why string, err error)
- func CutoverToFull(w BackupWindow, interval time.Duration, now time.Time) string
- func DefaultAuthPath() string
- func DefaultBaselineHistoryPath(serversPath string) string
- func DefaultConnectDraftPath(serversPath string) string
- func DefaultMCPTokenPath() string
- func DefaultRegistryPath() string
- func DefaultVerifyHistoryPath(serversPath string) string
- func DeriveServerName(host, port, flavor string) string
- func FullBackupPossible(e ServerEntry, gates BackupScheduleGates) error
- func FullCopySkipReason(reason string) string
- func FullCopyWhy(sched BackupSchedule) string
- func IsEmergencyWhyCode(code string) bool
- func IsFullCopySkip(reason string) bool
- func LeftOutKeys(left []LeftOut) []string
- func LocalKeepBlocked(entries []ServerEntry, e ServerEntry, excluded ...string) bool
- func LocalKeepTargets(entries []ServerEntry, excluded ...string) map[string]int
- func LocationWriters(entries []ServerEntry, e ServerEntry, cli CommandLineWriter) []string
- func NewTablesOf(names []string) (kept []string, omitted int)
- func NewTablesWhy(n int) string
- func NormalizeFlavor(s string) (string, error)
- func PGReplDSN(queryDSN string) (string, error)
- func RefusalReason(err error) string
- func RevokeMCPToken(path string) error
- func ScrubReason(msg string, secrets ...string) string
- func SetAuthPassword(path, username, password string) error
- func SharedLocationRefusal(entries []ServerEntry, e ServerEntry, cli CommandLineWriter) error
- func SplitVerifyTables(raw string) []string
- func ValidateBackupSetting(key, value string) error
- func ValidateNewPassword(password string) error
- func ValidateVerifyTables(raw string) error
- type AuthFile
- type BackupSchedule
- type BackupScheduleGates
- type BackupScheduleReporter
- type BackupScheduleState
- type BackupSettings
- type BackupSettingsDefaults
- type BackupWindow
- type BackupWindowProbe
- type BaselineController
- type BaselineRefreshDefaults
- type BaselineRefreshReporter
- type BaselineRequest
- type BaselineRestoreRequest
- type BaselineRestorer
- type BaselineRunHistory
- func (h *BaselineRunHistory) Append(rec BaselineRunRecord) error
- func (h *BaselineRunHistory) AppendCompact(rec BaselineRunRecord) (bool, error)
- func (h *BaselineRunHistory) AppendSkip(rec BaselineRunRecord) (bool, error)
- func (h *BaselineRunHistory) EmergencyStarted(serverID string) string
- func (h *BaselineRunHistory) FindBySnapshot(serverID, snapshotTime string) *BaselineRunRecord
- func (h *BaselineRunHistory) FullBackupFinished(serverID, snapshotTime string) time.Time
- func (h *BaselineRunHistory) IndexMarkFor(serverID, snapshotTime string) (uint64, bool)
- func (h *BaselineRunHistory) LastFullBackup(serverID string) time.Duration
- func (h *BaselineRunHistory) LastFullCopy(serverID string) (run, skip *BaselineRunRecord)
- func (h *BaselineRunHistory) LastFullRead(serverID string) *BaselineRunRecord
- func (h *BaselineRunHistory) LastScheduled(serverID string) (run, skip *BaselineRunRecord)
- func (h *BaselineRunHistory) LastSourceRead(serverID string, atOrBefore time.Time) time.Time
- func (h *BaselineRunHistory) List(serverID string) []BaselineRunRecord
- func (h *BaselineRunHistory) MeasuredSinceFull(serverID string) bool
- func (h *BaselineRunHistory) NoteEmergencyStart(serverID, at string) error
- func (h *BaselineRunHistory) Path() string
- func (h *BaselineRunHistory) ProvenUpdate(serverID string, within time.Duration) int64
- func (h *BaselineRunHistory) UpdateModel(serverID string) (fixed time.Duration, rate float64)
- func (h *BaselineRunHistory) UpdateSample(serverID string) (n int, newest time.Time)
- type BaselineRunRecord
- type BaselineStatus
- type CaptureStatus
- type CaptureStatusReporter
- type CommandLineWriter
- type Config
- type ConnectDraft
- type DoctorCheck
- type DoctorReport
- type DraftIdentity
- type DraftStore
- type FirstRunReport
- type FirstRunStep
- type FlashbackTarget
- type LeftOut
- type LocationMigration
- type MCPTokenFile
- type MonitorController
- type MonitorStatus
- type NewEntryDiscarder
- type ParsedBackupSchedule
- func (p ParsedBackupSchedule) BackupsPer30Days() int64
- func (p ParsedBackupSchedule) FullCopiesPer30Days() int64
- func (p ParsedBackupSchedule) FullSlotAtOrBefore(now time.Time) time.Time
- func (p ParsedBackupSchedule) NextFullRun(now time.Time) time.Time
- func (p ParsedBackupSchedule) NextRun(now time.Time) time.Time
- func (p ParsedBackupSchedule) SlotAtOrBefore(now time.Time) time.Time
- type RefusedTable
- type Registry
- func (r *Registry) Add(e ServerEntry) (ServerEntry, error)
- func (r *Registry) AddAutoNamed(e ServerEntry, base string) (ServerEntry, error)
- func (r *Registry) BackupSettings() BackupSettings
- func (r *Registry) CorrectSourceFlavor(id, detected string) (bool, error)
- func (r *Registry) DefaultBaselineDir(id string) string
- func (r *Registry) Delete(id string) error
- func (r *Registry) Get(id string) (ServerEntry, bool)
- func (r *Registry) Len() int
- func (r *Registry) List() []ServerEntry
- func (r *Registry) LocationMigration() LocationMigration
- func (r *Registry) MigrateProcessBaselineLocation(dir, s3 string) LocationMigration
- func (r *Registry) NameFor(typed, base string) string
- func (r *Registry) Path() string
- func (r *Registry) ReadOnly() bool
- func (r *Registry) Rotation() (RotationConfig, bool)
- func (r *Registry) SetBackupSetting(key string, value *string) error
- func (r *Registry) SetCommandLineWriter(dir, s3 string)
- func (r *Registry) SetProcessS3Location(label, loc string)
- func (r *Registry) SetRotation(rc RotationConfig) error
- func (r *Registry) UndoAdd(id string) error
- func (r *Registry) Update(e ServerEntry) error
- func (r *Registry) WriteRefusal(e ServerEntry) error
- type RotationConfig
- type RotationDefaults
- type SQLExportRequest
- type SQLExportStagedBuild
- type SQLExportStagingInfo
- type SQLExporter
- type ScheduleRefusal
- type SchemaSnapshotController
- type SchemaSnapshotRequest
- type SchemaSnapshotStatus
- type Server
- func (s *Server) Handler() http.Handler
- func (s *Server) Listen() (net.Listener, error)
- func (s *Server) NeedsSetup() bool
- func (s *Server) PasswordLogin() bool
- func (s *Server) ResolveFlashback(ctx context.Context, selector string) (FlashbackTarget, error)
- func (s *Server) Run(ctx context.Context) error
- func (s *Server) Serve(ctx context.Context, ln net.Listener) error
- func (s *Server) Token() string
- func (s *Server) URL() string
- type ServerEntry
- type SnapshotFailure
- type TelemetryController
- type VerifyCellDiff
- type VerifyController
- type VerifyExplanation
- type VerifyHistory
- type VerifyMode
- type VerifyRequest
- type VerifyRowDiff
- type VerifyRunRecord
- type VerifyStatus
- type VerifySummary
- type VerifyTableResult
Constants ¶
const ( // BackupMethodFull is a full backup from the source database (mydumper, // or pgbaseline for PostgreSQL): the same job the Create backup button // runs. It reads production and needs the console's baseline-creation // opt-in; when the server has an S3 destination it uploads there. BackupMethodFull = "backup" // BackupMethodRefresh rebuilds the newest backup from the recorded change // history alone (the same fold as --baseline-refresh-interval), reading // nothing from the source. It needs the server's own local directory to // write into; the previous backup it reads may be in the bucket, and on // a server whose backups go to S3 the scheduled path uploads its result // there afterwards (#1539). BackupMethodRefresh = "refresh" )
BackupMethod values name the producer a scheduled run used. They are the wire format of a run's method on the Snapshots page, not an input.
const ( // CaptureCaughtUp: the capture has checkpointed everything the source has // written, so an index that recorded nothing means the source changed // nothing. CaptureCaughtUp = "caught_up" // CaptureBehind: the source has written past the capture's checkpoint. CaptureBehind = "behind" )
Capture values (BackupWindow.Capture).
const ( BackupWhyNoIndex = "this server has no index connection to read the recorded changes from" BackupWhyNoLocalDir = "an update from the recorded changes needs a local snapshot directory" BackupWhyFirstBackup = "no previous snapshot to update" // BackupWhyUnreadablePrefix starts the reason for a full backup taken // because the previous one could not be read (the rest names the error). BackupWhyUnreadablePrefix = "the previous snapshot could not be read" // BackupWhyFoldRefusedPrefix / BackupWhyFoldCrashedPrefix start the reason // for a full backup that stands in for an update the fold refused or that // crashed (the rest names the refusal or the panic). BackupWhyFoldRefusedPrefix = "the update from the recorded changes was refused" BackupWhyFoldCrashedPrefix = "the update from the recorded changes hit an internal error" // BackupWhyWindowPrefix / BackupWhyStaleAnchorPrefix start the reason for // a full backup chosen over an update the daemon measured (#1721): the // update was estimated dearer than a full backup, or its anchor is past // the cut-over age with nothing measured. The rest carries the numbers. BackupWhyWindowPrefix = "an update from the recorded changes would take longer than a full read" BackupWhyStaleAnchorPrefix = "the previous snapshot is too old to update from" // BackupWhyFullCopyPrefix starts the reason for a full backup the // schedule's own full-copy timetable asked for (#1564); the rest names // the cadence. Not a fault, and the page does not treat it as one. BackupWhyFullCopyPrefix = "the schedule takes a full read" // BackupWhyNewTablesPrefix starts the reason for a full read taken // because an update left out tables created on the source after the // snapshot it started from (#1993); NewTablesWhy adds the count. BackupWhyNewTablesPrefix = "new tables were created on your database after the previous snapshot" )
The reasons a scheduled run takes a FULL backup instead of updating the previous one (#1604). They are persisted on the run record at run time, never recomputed for a past run: recomputed live, a bucket error that has since cleared shows the cheap producer for a run that read production in full. The first two are permanent until a setting changes, which is why the page turns them into the setting to change.
const ( // NewTablesActionFullRead: a full read is being started to include them. NewTablesActionFullRead = "full_read" // NewTablesActionNotPossible: a full read cannot start here // (FullBackupPossible refused; the reason is NewTablesActionReason). NewTablesActionNotPossible = "not_possible" // NewTablesActionGaveUp: full reads were started for them and kept // failing; no more are started on their own. NewTablesActionGaveUp = "gave_up" // NewTablesActionStillMissing: a full read went through after they were // first reported and still lacks them, so another would too. NewTablesActionStillMissing = "still_missing" // NewTablesActionNoSchedule: the update was the daemon-wide refresh, // which never takes a full read. NewTablesActionNoSchedule = "no_schedule" )
What the daemon decided about tables a published update left out (#1993), recorded on the update as NewTablesAction.
const ( BackupSettingBaselineRetain = "baseline_retain" BackupSettingLockMode = "lock_mode" BackupSettingStagingDir = "staging_dir" BackupSettingVerifyTables = "verify_tables" )
The saved keys, as the wire names them. They are the API's vocabulary and the YAML's, so they must not drift: backupSettingKeys is what the endpoint accepts and what the page's rows are keyed by.
const ( BaselineRunDump = "dump" // mydumper (or pgbaseline) snapshot of the source BaselineRunRefresh = "refresh" // periodic fold of the newest snapshot forward BaselineRunRestore = "restore" // operator-chosen point-in-time fold (#backups) BaselineRunCompact = "compact" // table-delta chains merged by the daemon's compaction job (#1723) )
Kind values for BaselineRunRecord. The literals are the file/wire format.
const ( // CaptureStateUpToDate: the source was asked, and capture has recorded // every transaction it has. CaptureStateUpToDate = "up_to_date" // CaptureStateBehind: the source was asked twice, and it has // transactions capture has not recorded. CaptureStateBehind = "behind" // CaptureStateUnknown: the source could not be asked, or what it said // does not settle it. Detail says which. CaptureStateUnknown = "unknown" )
const ( FlavorMySQL = "mysql" FlavorMariaDB = "mariadb" FlavorPostgres = "postgres" )
Source-family flavors. The registry records which capture engine a source needs: MySQL and MariaDB both index through the go-mysql binlog path (MariaDB differs only by GTID dialect at stream time), while PostgreSQL indexes through pgstreamrun. The string values are deliberately the SAME literals stream_state.flavor stores (see internal/query.SourceFlavor and recovery.DialectForFlavor). For MySQL and MariaDB the saved value is a hint: capture asks the server and follows it, and the console warns when the two disagree, so the saved label and stream_state.flavor CAN differ. Empty is treated as MySQL so every pre-#1019 registry entry keeps working — the same additive discipline as an empty SSLMode.
const ( // PageSnapshots is the one page Backups, Verification and Backup // settings became (#1573). A message that needs to be more precise // than the page names one of its sections in words ("under Set when // DBTrail starts"), which is what naming a different page used to do. PageSnapshots = "Snapshots" // PageConnect is where a reader downloads a DuckDB schema or exports // to Iceberg, on every console (#1573). PageConnect = "MCP Server" )
The console pages a message sends people to, by the name the sidebar shows. A message names a page through these, never as typed text: pages merge and get renamed, and a name typed in one message and missed in another sends people to a page that is not there. One message also trims its own page suffix off an error before appending it again (CheckBackupSchedule), which only works while both sides spell it the same.
const ( // SnapshotFailureMissingPermission: the source's database user lacks a // privilege the lock mode it last tried needs. Grant names it. SnapshotFailureMissingPermission = "missing_permission" // SnapshotFailureMydumperTooOld: the mydumper DBTrail runs is older than // this snapshot needs. MinVersion names the version to install. SnapshotFailureMydumperTooOld = "mydumper_too_old" )
const ( VerifyTriggerManual = "manual" VerifyTriggerScheduled = "scheduled" VerifyStateSkipped = "skipped" )
Trigger values for VerifyRunRecord, plus the history-only "skipped" state: live VerifyStatus.State never carries it — only scheduled cycles that could not run record it. Producers must use these consts; the literals are the wire/file format.
const ( VerifyStateIdle = "idle" VerifyStateRunning = "running" VerifyStateSucceeded = "succeeded" VerifyStateFailed = "failed" )
The rest of the VerifyStatus.State vocabulary. Named alongside VerifyStateSkipped because a consumer summarizing the history has to branch on all of them: "not skipped" is NOT "verified" — it also admits failed, and the two transient states a run can be caught in.
const BackupCutoverMinAge = 2 * time.Hour
BackupCutoverMinAge is the floor of the age rule: an update whose anchor is older than this (or than six schedule intervals, whichever is longer) is replaced by a full backup when nothing was measured. Six intervals so a daily schedule's normal one-day window is never "too old"; two hours so a five-minute schedule that fell hours behind (a long stop, #1721) does not fold the whole gap one page at a time.
const BackupProvenMargin = 1.5
BackupProvenMargin is how far past the largest update proven cheaper than a full backup (BackupWindow.Proven) the count may go before the estimate is believed over that evidence (#1736). A strict "more events than ever proven" was passed by a few thousand events at every slot of a steady load, where each window carries slightly more than the last, and the estimate it then believed was an artefact; within this margin the proven cost is the better evidence.
const BackupScheduleMinEvery = 5 * time.Minute
BackupScheduleMinEvery is the shortest interval a schedule accepts: a sanity bound a few ticks above the loop's one-minute clock, not a protection. It was 15m until #1620, when a server that folds from S3 wanted the cadence of a reporting copy. The floor does NOT stop the two costs that number once guarded against: a server with an S3 destination and no local backup directory takes a FULL backup every slot, one with no previous backup takes one on its first (ChooseBackupMethod), and a local-only server keeps every snapshot it publishes. All are said out loud instead, at save and at boot (warnBackupScheduleRate and its twin in backup_schedule_api.go) and on the card as the 30-day count, so the operator reads the rate before the source or the disk does.
const BackupSkipFullCopyPrefix = "the full read the schedule asks for did not start"
BackupSkipFullCopyPrefix starts every skip recorded for a slot of the full-backup timetable (#1564), whatever stopped it (a refusal, a job holding the server). Its own prefix because the page keeps it apart from the schedule's other skips: a skipped run is made up by the next run an hour later, a skipped weekly full backup by nothing for a week, so its line stays until a full backup starts again rather than until the next run ends.
const BackupWhyCodeFullCopy = "full_copy"
BackupWhyCodeFullCopy is BackupWhyCode's code for FullCopyWhy.
const BackupWhyCodeNewTables = "new_tables"
BackupWhyCodeNewTables is BackupWhyCode's code for NewTablesWhy.
const BaselineRunHistoryCap = 40
BaselineRunHistoryCap is how many baseline runs are kept per server. Old records fall off the front; the history answers "how long did this backup take, and who made it", not "archive every run forever".
const BaselineRunTriggerScheduled = "scheduled"
BaselineRunTriggerScheduled marks a run (or a skip) the per-server backup schedule started (#1442). Empty is everything else: the Create backup button, a restore, the daemon-wide refresh interval. The literal is the file/wire format and matches the verify history's vocabulary.
const DaemonBaselineS3Label = "DBTrail's --baseline-s3 default"
DaemonBaselineS3Label names the daemon's --baseline-s3 bucket in refusals and warnings about a store claiming it.
const DefaultLocalKeepNewest = 3
Every server keeps a local copy of its snapshots by default (#1681), in a folder of its own under DBTrail's state directory, and keeps the newest DefaultLocalKeepNewest of them when it has no external destination.
DefaultLocalKeepNewest is 3: two is the least that keeps a comparison possible (the verify check that compares one snapshot with the one before needs both), and the third is the spare for the moment the newest turns out to be the one that is wrong, so a bad run never leaves a single good copy. Unchanged tables are shared between snapshots as hard links, so each extra snapshot costs only the tables that changed.
Only NEW servers get it, at creation. An entry saved before this release has LocalKeepNewest 0 and keeps every local snapshot exactly as it always did: what happens to existing servers is a separate decision, and until it is made nothing changes for them. This constant is also the one switch that turns pruning off for new servers: 0 here means they are created with no local retention.
const DefaultSourceSSLMode = config.DefaultSourceSSLMode
DefaultSourceSSLMode is the TLS mode a source connection uses when its entry sets none: try TLS, and fall back to cleartext (with a warning) only when the server offers no TLS at all.
const MonitorErrEarlierCleanup = "earlier_cleanup_running"
MonitorErrEarlierCleanup is the ErrorCode of a start that waited its whole ceiling for a cleanup an earlier run left executing on the index server (#1708).
const RefusedTablesCap = 20
RefusedTablesCap is how many refused tables one run keeps by name. The rest are counted (RefusedTablesOmitted), never dropped silently: the run history holds BaselineRunHistoryCap runs per server in one file, and a server with thousands of tables refused at every slot would otherwise grow it without limit.
const VerdictLeftOut = "left-out"
VerdictLeftOut is the RefusedTable verdict of a table a full read left out of its snapshot (#2006).
const VerifyHistoryCap = 20
VerifyHistoryCap is how many runs are kept per server. Old records fall off the front — the history answers "when did this last verify, and how has it been trending", not "archive every run forever".
Exported because eviction is SILENT: a consumer summarizing the history has no other way to tell "these are all the runs there ever were" from "these are the newest VerifyHistoryCap of them". A server whose List is exactly this long must be reported as a possibly-truncated window, or a summary says "no failed runs" about a period whose failures fell off the front.
const ViewsSkippedCap = RefusedTablesCap
ViewsSkippedCap is how many names the detail of one snapshot carries. The rest are counted, as the refused tables of a run are (RefusedTablesCap).
Variables ¶
var ( // ErrDuplicateName rejects two registry entries sharing a name — the name // is the operator-facing label in the UI switcher, so it must be unique. ErrDuplicateName = errors.New("a server with this name already exists") // ErrUnknownServer is returned for an id with no registry entry. ErrUnknownServer = errors.New("unknown server id") // ErrRegistryReadOnly is returned for mutating operations when the on-disk // file was written by a newer bintrail (version > registryVersion). ErrRegistryReadOnly = errors.New("server registry was written by a newer bintrail; upgrade bintrail to edit it") // ErrS3StoreConflict: two servers name the same bucket but route it to // different stores (#1575). The store is per bucket, so one of them would // silently get the other's endpoint; the edit is refused instead. ErrS3StoreConflict = errors.New("S3 store conflict") )
var ErrAuthFileReadOnly = errors.New("auth file was written by a newer bintrail; logins work but changes are refused")
ErrAuthFileReadOnly is returned by write paths when the on-disk file was written by a newer bintrail (version > authFileVersion).
var ErrBackupScheduleNotRunnable = errors.New("the schedule cannot run")
ErrBackupScheduleNotRunnable is the class of every "this schedule cannot run on this daemon as configured" refusal; errors.Is matches it. The reason is carried separately (ScheduleRefusal) because it is what the page shows.
var ErrBaselineRunning = errors.New("a baseline is already running for this server")
ErrBaselineRunning is returned by BaselineController.Trigger when a baseline for that server is already in flight (one at a time per server). The handler maps it to 409 Conflict.
var ErrExplainRunning = errors.New("the drill-down for this table is still being computed")
ErrExplainRunning is returned by VerifyController.Explain while the drill-down for that table is still being computed. The handler maps it to 202 Accepted, and the caller polls the same URL until it answers 200, 404 (a newer run replaced the pair), or the caller's own cap.
This exists because the drill-down RE-RECONSTRUCTS the table (#1375): on a large table that is minutes of DuckDB work. The console itself tolerates that — it sets no WriteTimeout and apiGuard clears the read deadline — but a fronting reverse proxy at its stock read timeout (60s on nginx) does not, so behind one the operator's only experience was a request that died in the proxy: a button that silently did nothing. The work now runs on the daemon like a verify run does, and the request returns immediately either way.
ErrExplainUnavailable is returned by VerifyController.Explain when there is no cached baseline pair to explain from: no completed baseline-anchored run exists for this server, the table was not reported as a mismatch by it, or a newer run has since replaced it. The handler maps it to 404. The message names the newer-run case explicitly: a scheduled run (--verify-interval) that starts while a drill-down is still computing drops the previous run's cached pairs, so an operator mid-wait can land here with a verify actually in flight. Without that clause the text tells them to "run a baseline-anchored verify first" while one is running — advice that sends them looking for a problem that does not exist.
var ErrMCPTokenFileReadOnly = errors.New("MCP token file was written by a newer bintrail; changes are refused")
ErrMCPTokenFileReadOnly is returned by write paths when the on-disk file was written by a newer bintrail (version > mcpTokenFileVersion).
var ErrSchemaSnapshotRunning = errors.New("a schema snapshot is already running for this server")
ErrSchemaSnapshotRunning is returned by SchemaSnapshotController.Trigger when one is already in flight for that server (one at a time per server). The handler maps it to 409 Conflict.
ErrSharedLocation is the refusal of a write into a shared location.
var ErrUnknownBackupSetting = errors.New("unknown snapshot setting")
ErrUnknownBackupSetting rejects a key this build does not model. Refusing is deliberate: silently accepting a key would store a value nothing reads, and the interface would show a setting in force that governs nothing.
var ErrVerifyRunning = errors.New("a verify run is already running for this server")
ErrVerifyRunning is returned by VerifyController.Trigger when a verify run for that server is already in flight (one at a time per server, mirroring BaselineController). The handler maps it to 409 Conflict.
Functions ¶
func BackupCutoverAge ¶ added in v0.84.0
BackupCutoverAge is the anchor age past which the age rule cuts over, for a schedule that fires every interval (zero interval: the floor).
func BackupSettingKeys ¶ added in v0.85.0
func BackupSettingKeys() []string
BackupSettingKeys lists the editable keys in wire order.
func BackupWhyCode ¶ added in v0.82.0
BackupWhyCode classifies a persisted full-backup reason for the page, so the remedy is chosen by a stable code and not by matching prose that may be reworded later. Classified when the record is WRITTEN, so an old record keeps the code its wording had then; the loop's live view of a job classifies the same captured wording again in the same build, which is the only surface for a full backup that panicked and wrote no record. Empty for a reason this build does not know (an update, or a newer daemon's wording).
func BaselineFoldSource ¶ added in v0.74.0
func BaselineFoldSource(e ServerEntry) string
BaselineFoldSource is where a fold for e reads its PREVIOUS snapshot from, which is not always where it writes the new one.
The two diverge exactly when backups go to S3 (#1539): the previous snapshot lives in the bucket, so a fold that read the local directory would find an empty one on a server whose backups have only ever been uploaded, and report "no previous snapshot to update" while the bucket holds dozens. FindBaseline and ListBaselines both dispatch on the s3:// prefix, so the remote source needs no separate code path here.
Note this is NOT how time-travel resolves a baseline: bundle.findBaseline prefers the local directory and treats the bucket only as a per-table fallback (#766), so on a server with both, a table whose local copy is stale is read stale by time-travel and fresh by the fold. The Backups listing merges both locations (#1571) and the point-in-time restore reads this same source (#1541); do not "fix" the divergence by making the fold read local. The daemon's refresh loop DOES read the local copy for one run when that copy is the bucket's newest snapshot, table for table (consoleapp resolveFoldSource, #1626): never a stale copy, only the same one, so unchanged tables can be carried forward by hard link.
The OUTPUT stays local whatever this returns: the fold writes Parquet to a filesystem, and the upload is a separate step afterwards.
func CheckBackupSchedule ¶ added in v0.71.0
func CheckBackupSchedule(e ServerEntry, sched BackupSchedule, gates BackupScheduleGates) error
CheckBackupSchedule reports whether e's schedule can run on a daemon with these gates, and why not. Nil error means at least one producer can run. Called on PUT (so a schedule that could never run is refused with the reason instead of saved) and on every listing (so a schedule the environment later invalidated is reported, not silently skipped by the loop).
func CheckFullCopy ¶ added in v0.86.0
func CheckFullCopy(e ServerEntry, sched BackupSchedule, gates BackupScheduleGates) error
CheckFullCopy reports whether the full-copy timetable of e's schedule (#1564) can take its full backups on a daemon with these gates, and why not; nil without one. Separate from CheckBackupSchedule on purpose: a full copy that cannot start must not stop the scheduled updates, which is what a refusal there would do. It is REFUSED LOUDLY instead: a save is refused with this reason, and a saved schedule the environment later invalidated (the creation opt-in turned off at a restart) carries it on the page before the slot, and records a skip naming it at the slot. Silence would be an operator's weekly independent copy quietly never happening.
A process that runs no loop is CheckBackupSchedule's refusal to report, and nil here, so the page does not say the same thing twice.
func ChooseBackupMethod ¶ added in v0.71.0
func ChooseBackupMethod(ctx context.Context, e ServerEntry, gates BackupScheduleGates) (method, why string, err error)
ChooseBackupMethod decides how the next scheduled run for e will be made, and why, on a daemon with these gates. The rule, in order:
- a server an update cannot serve (no index DSN, no local backup directory) gets a FULL backup, with that refusal as the why;
- a server with no previous backup gets a FULL backup: there is nothing to update. A directory that does not exist yet is that case; one that cannot be READ is its own error, never "no snapshot yet". The previous backup is looked for where the fold would READ it (BaselineFoldSource), which is the bucket on an S3-backed server;
- a server whose update the daemon measured as dearer than a full backup, or whose previous backup is past the cut-over age with nothing measured (#1721, CutoverToFull), gets a FULL backup with the numbers as the why — only when a full backup can start; otherwise the update runs, slow as it may be, because it is the producer that can;
- otherwise the newest backup is UPDATED from the recorded changes, with no load on the source. If that update fails (a capture gap, a schema change, a crash), the loop takes a full backup at the same slot when the daemon may take one, and records a skip naming both otherwise.
A rule that picks a producer the daemon cannot run (the opt-in off, no source) is reported as such; the caller decides whether that is a skip.
func ChooseBackupMethodAt ¶ added in v0.84.0
func ChooseBackupMethodAt(ctx context.Context, e ServerEntry, gates BackupScheduleGates, now time.Time) (method, why string, err error)
ChooseBackupMethodAt is ChooseBackupMethod deciding for a slot at now: the loop passes its tick, so the age rule reads the slot's clock and a test can pin it.
func CutoverToFull ¶ added in v0.84.0
CutoverToFull is the #1721 rule: the why for a FULL backup instead of an update, or "" to update. Measured first: with the events since the anchor, a measured fold rate and a last full backup on record, the update is cut over when its estimate exceeds the full backup's duration, and NOT cut over otherwise, however old the anchor (the estimate is the better evidence). Only without one of the three does the age rule apply: an anchor older than BackupCutoverAge(interval), counted from when the full backup that published it finished when one did (AnchorFullFinished). The estimate is crude on purpose (events × rate, as the issue asked): it decides between two producers, not a schedule.
While no update has been measured since the last full backup (UnmeasuredSinceFull, #1737) the model may still say "update", and that stands; what it may not do is choose a full backup, since the numbers it would choose one on are the ones that chose the last. The age rule still applies. On a server whose updates really do cost more than a full backup that is one update after each full backup, the price of a model that can notice when they stop.
func DefaultAuthPath ¶ added in v0.11.0
func DefaultAuthPath() string
DefaultAuthPath returns ~/.config/bintrail/console-auth.yaml, with the same homeless fallback as DefaultRegistryPath (see configPath).
func DefaultBaselineHistoryPath ¶ added in v0.65.0
DefaultBaselineHistoryPath returns the history file path as a sibling of the server registry file, so --console-servers-file relocations carry it.
func DefaultConnectDraftPath ¶ added in v0.87.0
DefaultConnectDraftPath names the draft file as a sibling of the server registry, like the verify and backup run histories. An empty registry path (an in-memory registry) has no directory to be a sibling of, and must not spill a half-filled form into the working directory: it stays in memory.
func DefaultMCPTokenPath ¶ added in v0.40.0
func DefaultMCPTokenPath() string
DefaultMCPTokenPath returns ~/.config/bintrail/console-mcp-token.yaml, with the same homeless fallback as DefaultAuthPath (see configPath).
func DefaultRegistryPath ¶
func DefaultRegistryPath() string
DefaultRegistryPath returns ~/.config/bintrail/console-servers.yaml, with configPath's working-directory fallback for homeless environments. The verify and baseline run histories are named as siblings of this path, so they inherit whatever it resolves to.
func DefaultVerifyHistoryPath ¶ added in v0.49.0
DefaultVerifyHistoryPath returns the history file path as a sibling of the server registry file, so `--console-servers-file` relocations carry both.
func DeriveServerName ¶ added in v0.87.0
DeriveServerName builds the name a new server gets when nobody typed one, from the address of the database it reads. It returns "" when there is no host to derive from; the caller decides what that means (the registry still refuses a nameless entry).
The host box does not always hold a bare hostname, so three shapes are unpicked before anything else: a host with the port typed into it ("db:3307"), an IPv6 literal in brackets ("[::1]"), and both together ("[::1]:3307"). An explicitly typed port box wins over a port found inside the host, because it is the field the person aimed at.
What comes out carries only lowercase letters, digits, dot, underscore and dash. That is not a security boundary — nothing in the product uses a server name as a path or a key, the id does that — it is so the label reads the same everywhere it is shown.
func FullBackupPossible ¶ added in v0.71.0
func FullBackupPossible(e ServerEntry, gates BackupScheduleGates) error
FullBackupPossible reports whether a full backup can start for e on a daemon with these gates, and why not. Exported for the loop's fallback decision, which needs exactly this half of ChooseBackupMethod.
func FullCopySkipReason ¶ added in v0.86.0
FullCopySkipReason is reason as a full-backup timetable skip records it.
func FullCopyWhy ¶ added in v0.86.0
func FullCopyWhy(sched BackupSchedule) string
FullCopyWhy is the reason a scheduled full copy records (#1564).
func IsEmergencyWhyCode ¶ added in v0.95.0
Append records one run and saves the file (oldest dropped past the cap). A save failure is returned for the caller to log; history is an observability aid and must never fail the run it describes. IsEmergencyWhyCode reports whether a full read's recorded reason makes it an emergency read (#2006): one the schedule took on its own because an update was refused or crashed, or for tables an update left out.
func IsFullCopySkip ¶ added in v0.86.0
IsFullCopySkip reports whether a recorded skip reason is one of the full-backup timetable's (BackupSkipFullCopyPrefix).
func LeftOutKeys ¶ added in v0.95.0
LeftOutKeys is every left-out table the source can be matched against, as "schema.table", or ".table" when the schema's real name is unknown; one whose table name is unknown cannot be matched and is not listed. Uncapped: the new-tables check (#1998) needs all of them, not the 20 the page shows.
func LocalKeepBlocked ¶ added in v0.87.0
func LocalKeepBlocked(entries []ServerEntry, e ServerEntry, excluded ...string) bool
LocalKeepBlocked reports whether e's folder is one LocalKeepTargets refuses to prune whatever e's count says: shared with another server, once shared and still holding its snapshots, or the daemon's own folder. The settings row says so instead of promising a count.
func LocalKeepTargets ¶ added in v0.87.0
func LocalKeepTargets(entries []ServerEntry, excluded ...string) map[string]int
LocalKeepTargets is the ONE rule for which local folders the daemon prunes down to a keep-newest count (#1681), shared by the prune loop, the snapshot listing that reports the policy and the settings row that describes it, so none of them can announce a retention the loop does not apply, or miss one it does.
A folder qualifies when exactly ONE server keeps its snapshots there (BaselineDir), that server has no external destination (BaselineS3 empty) and a count (LocalKeepNewest > 0), and the folder is not one of excluded (the daemon's own --baseline-dir). A folder two servers share is never pruned: a snapshot does not record which server wrote it, so one server's newer copy of a table would count as the newest copy of the other's. Folders are compared after resolving symlinks, so a second spelling of the same folder is still the same folder. A folder that USED to be shared and still holds the other server's snapshots (heldNow) is never pruned either: the reason above outlives the sharing.
func LocationWriters ¶ added in v0.92.0
func LocationWriters(entries []ServerEntry, e ServerEntry, cli CommandLineWriter) []string
LocationWriters returns the other writers of e's snapshot location, by name, in registry order, the command-line server last. Empty means e is the only one. Folders are compared as canonicalDir resolves them, S3 locations as the same prefix: a listing reads only the timestamp-named folders right under its prefix, so a nested prefix (s3://b/p/sub under s3://b/p) never mixes with its parent, exactly as a subfolder does not.
func NewTablesOf ¶ added in v0.94.0
NewTablesOf keeps the names of the tables a published update left out (#1993), sorted as given, up to RefusedTablesCap, and counts the rest: the same bound and the same reason as RefusedTablesOf (a migration that creates hundreds of tables must not grow the run history without limit).
func NewTablesWhy ¶ added in v0.94.0
NewTablesWhy is the reason recorded on the full read a scheduled update starts to include the n tables it left out (#1993). A count, never the names: the reason is served to every session that can see the schedule, and a session with a data profile is not given table names (the run's own NewTables list is withheld from it, withholdScheduleTables).
func NormalizeFlavor ¶ added in v0.33.0
NormalizeFlavor maps a request/registry flavor string to a canonical value. Empty → mysql; "postgresql" is accepted as an alias for "postgres". An unrecognized value is an error so a create/update rejects a typo rather than silently mis-routing capture.
func PGReplDSN ¶ added in v0.33.0
PGReplDSN derives the replication (walsender) connection string from a stored PostgreSQL query DSN by adding replication=database. pgcapture/pgstreamrun and pgbaseline require a distinct replication connection (a CopyBoth conn that cannot run ordinary SQL); the console stores only the query DSN and derives this one, so the replication-param logic lives in exactly one place. Consumed by the monitor supervisor (#1020) and the baseline trigger (#1021).
Pure net/url — no pgx: internal/console is on the read side of the dependency-guard (internal/event/depguard_test.go) and must never link the PostgreSQL capture libraries. A postgres:// URL round-trips through net/url and re-parses cleanly in pgconn.ParseConfig downstream.
func RefusalReason ¶ added in v0.71.0
RefusalReason returns the reason inside a ScheduleRefusal, or the whole message for any other error.
func RevokeMCPToken ¶ added in v0.40.0
RevokeMCPToken removes the managed-token file. A missing file is a no-op, a MALFORMED file is removed with a warning (revoking junk is still revoking), an unreadable file or a newer-version file refuses — deleting requires only directory permission, so an unreadable file could otherwise be destroyed without ever checking the read-only contract.
func ScrubReason ¶ added in v0.94.0
ScrubReason is the treatment RefusedTablesOf gives a reason, for an error text that is not a table's: one line, at most refusedReasonCap runes, and each connection string in secrets, its password and any credentials left in a URL or DSN removed. It reaches a browser and a file on disk.
func SetAuthPassword ¶ added in v0.11.0
SetAuthPassword hashes password (bcrypt, cost consoleBcryptCost) and writes the credential file atomically, preserving any Extra fields a newer binary stored. username == "" keeps the existing username (default "admin" on first set). Refuses a newer-version file with ErrAuthFileReadOnly.
func SharedLocationRefusal ¶ added in v0.92.0
func SharedLocationRefusal(entries []ServerEntry, e ServerEntry, cli CommandLineWriter) error
SharedLocationRefusal is the refusal for a write by e into a location others also write, or nil. It names them and the fix.
func SplitVerifyTables ¶ added in v0.85.0
SplitVerifyTables parses the comma-separated verify-tables list; empty entries are dropped, and an empty list means no filter (nil).
Exported from here, and called by the daemon's flag parsing, so the two halves cannot disagree about what the list means: a filter the page accepts and the loop reads differently would silently verify the wrong tables.
func ValidateBackupSetting ¶ added in v0.85.0
ValidateBackupSetting refuses a value the daemon could not act on, BEFORE it reaches the file. Storing an unparseable value would be worse than refusing it: the page would show it as the setting in force while every consumer fell back to the flag, which is a setting that reads as changed and is not.
The grammars are the daemon's own parsers, not copies. A second spelling of "7d" or of a lock mode would drift, and the drift would only show as a warning in a log nobody is reading while the page says the value is live.
An empty value is always accepted: emptiness is how a setting is turned off, and each consumer already defines what off means (no pruning, a temporary directory, no table filter).
func ValidateNewPassword ¶ added in v0.11.0
ValidateNewPassword enforces the set-time policy: at least minPasswordChars characters and at most 72 bytes (bcrypt's hard limit — reject, never truncate).
func ValidateVerifyTables ¶ added in v0.85.0
ValidateVerifyTables refuses a list whose entries are not schema.table. A typo here does not fail loudly at run time — the table simply never matches and verification quietly covers less than the operator believes.
Types ¶
type AuthFile ¶ added in v0.11.0
type AuthFile struct {
Version int `yaml:"version"`
Username string `yaml:"username"`
PasswordBcrypt string `yaml:"password_bcrypt"`
UpdatedAt string `yaml:"updated_at"`
// Extra preserves unknown top-level fields a newer binary wrote, across
// this binary's load→save cycle (registry.go precedent; inline requires
// map[string]any).
Extra map[string]any `yaml:",inline"`
// contains filtered or unexported fields
}
AuthFile is the on-disk console credential: one username and one bcrypt hash. The console is single-user by design (multi-user/RBAC is dbtrail territory); the versioned envelope and Extra leave room for additive fields without a format break.
func LoadAuthFile ¶ added in v0.11.0
LoadAuthFile reads the credential file at path. A missing file returns (nil, nil): password login not configured. A present-but-unreadable or unparseable file is a loud error — silently degrading to token-only when the operator configured a password would be an auth downgrade.
func (*AuthFile) ReadOnly ¶ added in v0.11.0
ReadOnly reports whether write paths must refuse (newer-version file).
func (*AuthFile) VerifyPassword ¶ added in v0.11.0
VerifyPassword checks username+password against the stored credential in constant time: the username compare is subtle.ConstantTimeCompare and a username mismatch STILL runs a full bcrypt compare (against the dummy hash) so timing cannot enumerate the username. Safe on a nil receiver (false).
type BackupSchedule ¶ added in v0.71.0
type BackupSchedule struct {
// Every is the interval: Nm, Nh or Nd (cliutil.ParseInterval).
Every string `yaml:"every"`
// At is the UTC clock time the grid is aligned to, HH:MM. Empty means
// 00:00. For whole days (1d, 7d) it is the time of day the backup runs;
// for an interval that divides a day (6h, 15m) it is the alignment of the
// slots; an interval that does not divide a day evenly (5h, 36h) drifts
// through the day, and the page shows the next run so that is visible.
At string `yaml:"at,omitempty"`
// FullEvery is an optional second timetable (#1564), in the same grammar
// as Every and on the same At: at each of its slots the run is a full
// backup from the source, whatever ChooseBackupMethod would pick. The
// automatic choice only takes a full backup when an update cannot serve
// (a first backup, a gap, a schema change), which are faults or firsts;
// this is the operator asking for one: a read of the database on a stated
// cadence that does not rest on any earlier backup or on the recorded
// changes, which cuts the chain of updates and whatever it inherited.
// Empty for none. At least Every: a full copy takes a scheduled run's
// place, so asking for more of them than runs is asking for every run to
// be one, which Every already says.
FullEvery string `yaml:"full_every,omitempty"`
// FullSince is when the operator set FullEvery to its current value
// (RFC3339 UTC), stamped by the API, never by the operator. It bounds what
// the page reports about the timetable: a miss recorded under an earlier
// timetable, or before one existed, is not this one's, and a slot before
// it never belonged to it (the boot check that records a slot missed
// while the daemon was down relies on that). Empty on a schedule saved
// without it: nothing is bounded, and the boot check stays silent rather
// than guess. Not part of Identity: saving the same timetable again does
// not restart its grid.
FullSince string `yaml:"full_since,omitempty"`
// Extra preserves future keys the way ServerEntry.Extra does.
Extra map[string]any `yaml:",inline"`
}
BackupSchedule is the per-server schedule as stored in the registry. The fields hold the operator-typed strings so they round-trip exactly; Parse turns them into durations with the same grammar as the daemon flags.
func (BackupSchedule) Identity ¶ added in v0.71.0
func (b BackupSchedule) Identity() string
Identity is the schedule as a comparable string (every|at, with the defaults resolved), for a consumer that must notice a CHANGED schedule: the loop treats an edit as a new schedule, so the "first observation is silent" rule covers edits the way it covers adds. Unparseable schedules identify by their raw fields.
func (BackupSchedule) Normalized ¶ added in v0.71.0
func (b BackupSchedule) Normalized() (BackupSchedule, error)
Normalized returns the schedule with its defaults spelled out, for storage: "" becomes "00:00", so the file says what runs.
func (BackupSchedule) Parse ¶ added in v0.71.0
func (b BackupSchedule) Parse() (ParsedBackupSchedule, error)
Parse validates the schedule and resolves its defaults. Every error names the field and the accepted grammar, because the message is what the Backups page shows back.
type BackupScheduleGates ¶ added in v0.71.0
type BackupScheduleGates struct {
// LoopRunning: this process runs the schedule loop at all (a watch daemon
// with a baseline supervisor).
LoopRunning bool
// FullBackups: this process may take full backups from the source (the
// baseline-creation opt-in). A rebuild does not need it.
FullBackups bool
// FullBackupsErr, when set, is why a full backup cannot START even with
// the opt-in on: the lock-mode misconfiguration the supervisor refuses
// every MySQL dump with. Read when the gates are built, so a lock mode
// fixed from Backup settings clears it without a restart.
FullBackupsErr string
// ReadOnlyConsole: this process is the standalone `serve` console, which
// runs no loop of any kind; names the daemon in the reason.
ReadOnlyConsole bool
// Window measures what an update would have to fold for one server, so
// ChooseBackupMethod can cut over to a full backup when the update is
// the dearer producer (#1721). Nil: never cut over (a process with no
// measurements, or a test).
Window BackupWindowProbe
// WriteRefusal is why e may not write snapshots at all: its location is
// one another server writes too (#1684, Registry.WriteRefusal). It beats
// every producer, the update included. Nil: no such check (a test).
WriteRefusal func(ServerEntry) error
}
BackupScheduleGates is what the daemon can do, as the schedule checker needs to know it. Decided at boot, except FullBackupsErr, which follows the lock mode a dump would use each time the gates are built.
type BackupScheduleReporter ¶ added in v0.71.0
type BackupScheduleReporter interface {
ScheduleState(serverID string) BackupScheduleState
// FullBackups reports whether the loop may run full backups (the
// daemon's baseline-creation opt-in), and, when it may, whether the
// supervisor would still refuse to start one (a lock-mode
// misconfiguration): nil when full backups can start.
FullBackups() (enabled bool, refusal error)
// Observe tells the loop a schedule was saved at `at`, so the slot in
// progress then counts as seen and the NEXT boundary fires, even one
// that falls before the loop's next tick. Without it the next_run the
// page showed at save time could be silently skipped.
Observe(serverID string, sched BackupSchedule, at time.Time)
// Forget tells the loop the schedule for serverID was removed, so its
// observation and last outcome are dropped now, not at the next tick.
Forget(serverID string)
// WindowProbe measures what an update would fold (#1721), for
// ChooseBackupMethod; nil when this process measures nothing.
WindowProbe() BackupWindowProbe
}
BackupScheduleReporter is the schedule loop as the console sees it. nil when this process runs no loop (the read-only console, or a watch daemon with every baseline feature off), which the schedule endpoints refuse on and the listing reports as not runnable.
type BackupScheduleState ¶ added in v0.71.0
type BackupScheduleState struct {
// LastStartedAt is when the loop last started a job for this server
// (RFC3339 UTC), empty if never in this process.
LastStartedAt string
// LastMethod is that job's producer (BackupMethodFull/Refresh).
LastMethod string
// LastWhy is why that job was a full backup (#1604), as decided when it
// started; empty for an update.
LastWhy string
// Last is the supervisor's status for THAT job: live while it is in the
// slot, afterwards the copy ScheduleState took when it saw the job
// terminal (so a later manual job taking the slot does not erase it),
// nil when nothing was started here. The copy is taken on READ; the
// loop's watcher is the read that always happens, so it exists for
// every scheduled job, not only the ones a page load caught in time.
Last *BaselineStatus
// Running: the job this schedule last started has not finished.
Running bool
// LastSkippedAt / LastSkipReason describe the last slot this process
// could not start, empty if none. The history has the durable copy.
LastSkippedAt string
LastSkipReason string
// LastFullMissedAt / LastFullMissedReason describe the last slot of the
// full-backup timetable that did not start (#1564), with the reason as
// recorded (BackupSkipFullCopyPrefix). Kept apart from LastSkipped and
// cleared only when a full backup of the timetable starts, not when the
// next run ends: this process only; the history has the durable copy.
LastFullMissedAt string
LastFullMissedReason string
// FullOwed: a slot of the full-backup timetable found another job
// holding the server, so the next scheduled run takes the full backup
// instead of waiting a whole FullEvery for the next slot (#1564). This
// process only: a restart drops the debt, and the recorded miss then
// stays on the page until a full read of the database succeeds.
FullOwed bool
// LastFallbackAt / LastFallbackReason describe the last slot where the
// update from the recorded changes failed and a full backup was STARTED
// in its place (never a collision, which is a skip); cleared when a
// later scheduled job other than that full backup succeeds. This
// process only.
LastFallbackAt string
LastFallbackReason string
// What the failed update reported, kept with the fallback because the
// status slot it was read from belongs to the next run (#1653).
LastFallbackTables int
LastFallbackRefused int
LastFallbackRefusedTables []RefusedTable
LastFallbackRefusedOmitted int
// LastFallbackStoppedAt: the update was refused again for the same
// tables right after the fallback's full read went through, so the loop
// takes no more full reads for it (#2006). Set from then until an update
// goes through; the Refused* fields above then describe the newest
// refusal. This process only.
LastFallbackStoppedAt string
}
BackupScheduleState is the schedule loop's in-memory view of one server, for the Snapshots page: the job the schedule last started in this process, as the supervisor's slot reports it. The durable view (runs and skips across restarts) is the baseline run history; this one exists because the history can be unavailable (an unreadable file) or silent (a job that panicked writes no record), and a failed scheduled backup must reach the page either way.
type BackupSettings ¶ added in v0.85.0
type BackupSettings struct {
// BaselineRetain prunes local snapshots older than this once a durable
// copy exists elsewhere (--baseline-retain).
BaselineRetain *string `yaml:"baseline_retain,omitempty"`
// LockMode is the lock mydumper takes for a full backup
// (BINTRAIL_CONSOLE_BASELINE_LOCK_MODE).
LockMode *string `yaml:"lock_mode,omitempty"`
// StagingDir is where a .sql export is assembled before it is served
// (BINTRAIL_CONSOLE_BASELINE_STAGING).
StagingDir *string `yaml:"staging_dir,omitempty"`
// VerifyTables restricts the verification loop to these tables
// (--verify-tables).
VerifyTables *string `yaml:"verify_tables,omitempty"`
// Extra preserves keys a newer binary stored here, the same way
// ServerEntry.Extra and the envelope's own Extra do.
Extra map[string]any `yaml:",inline"`
}
BackupSettings is the console-editable half of the daemon-wide backup configuration (#1682): values that used to exist ONLY as flags and environment variables of the process, which on this product means that changing one costs a restart — and a restart of this process stops change capture.
It lives in the registry envelope rather than in a new file. The premise that the registry is per server by construction is not true: the envelope already carries two daemon-wide sections the interface writes (`rotation` and `baseline_refresh`), with the atomic-write discipline, the version gate and the Extra catch-all that a settings file would have had to reinvent. A second file would have meant two homes for one question.
Every field is a POINTER, and that is the whole tri-state: "nothing saved" (the flag or environment variable is in force) has to be distinguishable from "saved as empty" (the operator turned it off), or clearing a value in the interface would silently hand the setting back to the flag that set it. Same reasoning the removed baseline-refresh override had for its pointer (#1681), one level deeper: here each row is saved on its own, so the tri-state has to be per row.
type BackupSettingsDefaults ¶ added in v0.77.0
type BackupSettingsDefaults struct {
BaselineRetain string // --baseline-retain
RefreshEvery string // --baseline-refresh-interval
LockMode string // BINTRAIL_CONSOLE_BASELINE_LOCK_MODE
// LockModeErr: the env value was rejected, so LockMode holds the
// fallback default, which is NOT in force — MySQL dumps are refused
// while it stands. The page must show the rejection, not the fallback.
LockModeErr string
TriggerOn bool // BINTRAIL_CONSOLE_BASELINE_TRIGGER
StagingDir string // BINTRAIL_CONSOLE_BASELINE_STAGING
VerifyInterval string // --verify-interval
VerifyTables string // --verify-tables
// Live names the keys THIS process applies without a restart, because
// only the process knows: liveness is a property of how the daemon reads
// the value (a provider consulted per job) and not of the setting. The
// read-only `serve` passes none, which is correct — it runs no loops.
// Keeping this here rather than hardcoding a list in the page is what
// stops the interface from promising a live edit a daemon never applies.
Live []string
}
BackupSettingsDefaults carries the daemon-wide flag/env values the page reports, injected by the watch daemon exactly like RotationDefaults: what the process was TOLD, verbatim, so the page never re-derives it. Zero on the standalone serve, whose page hides the daemon card (no monitor capability); the per-server half of the page renders there regardless.
type BackupWindow ¶ added in v0.84.0
type BackupWindow struct {
// Anchor is the previous snapshot's instant (the one an update would
// start from); zero when unknown.
Anchor time.Time
// AnchorFullFinished is when the full backup that published Anchor
// finished, when one did and this daemon recorded it; zero otherwise. A
// full backup's snapshot is named for the instant its dump STARTED, so
// its anchor is already as old as the backup took when the next slot
// comes, and the age rule counts from here instead (#1737): the time a
// full backup took is not time the schedule fell behind. Without this a
// full backup longer than the cut-over age was followed by another one
// on age at every slot, and the update that would correct the model
// never ran.
AnchorFullFinished time.Time
// Capture says whether the source has written anything the capture has
// not checkpointed (#1791): CaptureCaughtUp, CaptureBehind, or empty when
// not known. The daemon asks only when it matters, nothing indexed since
// the anchor (Events == 0) on an anchor past the cut-over age, because
// from the index alone "the source wrote nothing" and "the capture
// stopped" are the same observation (#1223): the binlog syncer retries
// without limit and the checkpoint ticker keeps stamping while it does.
// CaptureDetail says why, for the reason line, when not caught up.
Capture string
CaptureDetail string
// Events is how far the index's high-water mark has moved since the
// anchor; negative when unknown (no mark on record for THIS anchor, or
// the index did not answer). It counts every source writing to that
// index and any rows a resumed capture re-inserted, not the rows this
// server's fold would apply: the same over-count is in the rate, so the
// two cancel while the traffic mix holds.
Events int64
// FoldFixed and FoldRate model what an update costs from the run
// history: fixed seconds every update pays (starting DuckDB, reading
// every table's previous Parquet, fetching it from the bucket) plus one
// second per FoldRate events. Zero when unknown, which UpdateModel
// decides five ways: fewer than two measured updates, or the runs'
// times beyond the shortest under a tenth of their total (a quiet
// server: every update costs the fixed cost), or their events beyond
// the shortest under a tenth of a typical run's (a steady load: every
// update applies the same), or a marginal rate under a tenth of the
// shortest run's whole rate (noise in the denominator, #1736), or no
// run that applied at least ten thousand events more than the shortest
// (a quiet server whose one slightly larger update took a noisy while
// longer, #1738). Those cover a flat axis, a noisy denominator and a
// small one; they do not cover noise that happens to line up with a
// wide spread of events, which only more samples dilute. A rate read
// off any of them would be an artefact that cuts over on every burst,
// or on every slot. FoldFixed alone still decides one thing without a
// rate: an update whose cheapest measured run already took longer than
// the last full backup.
FoldFixed time.Duration
FoldRate float64
// UnmeasuredSinceFull: no measured update was recorded after the newest
// full backup, so the model (FoldFixed, FoldRate, Proven) is older than
// it, and it may not choose a full backup (#1737); it may still say
// "update". Without this, a full backup the model chose was followed by
// one unmeasured update, and the slot after that chose a full backup on
// the same numbers again: a rate that had gone wrong could never be
// corrected. False (the zero value) when a measured update is newer, or
// when there is no full backup on record.
UnmeasuredSinceFull bool
// Proven is the largest number of events one of the newest five
// measured updates (the model's own sample) applied in less time than
// the last full backup took. An update up to BackupProvenMargin times
// that size is not cut over on the estimate: evidence beats the model,
// with a margin for the few thousand events each window carries over
// the last (#1736).
Proven int64
// LastFull is how long the last successful full backup took; zero when
// there is none on record.
LastFull time.Duration
}
BackupWindow is what a scheduled update would have to fold, as the daemon measured it. Every field may be unknown, and the rule (CutoverToFull) says which combinations it can decide on.
type BackupWindowProbe ¶ added in v0.84.0
type BackupWindowProbe func(ctx context.Context, e ServerEntry, anchor time.Time) BackupWindow
BackupWindowProbe measures the window for e whose previous snapshot is anchor. It runs on every decision, page loads included, so it must be cheap and must answer "unknown" rather than block.
type BaselineController ¶ added in v0.21.0
type BaselineController interface {
// Trigger starts a baseline in the background and returns immediately.
// Returns ErrBaselineRunning if one is already running for req.ServerID.
Trigger(req BaselineRequest) error
// Status reports the latest known state for a server (idle if never run in
// this process — the durable record is the snapshot itself, listed by
// /api/baselines).
Status(serverID string) BaselineStatus
}
BaselineController runs a baseline snapshot for a monitored server entirely in-process — dump → convert → upload — so the console never shells a sibling container and never mounts the docker socket (#613). It is wired in ONLY by `bintrail-console watch` when the operator opts in (BINTRAIL_CONSOLE_BASELINE_TRIGGER=1); nil on the standalone read-only console, where the endpoint refuses with 403 — mirroring how MonitorController gates the monitor verbs.
type BaselineRefreshDefaults ¶ added in v0.70.0
type BaselineRefreshDefaults struct {
// CarryForwardUnchanged is the daemon's --baseline-carry-forward-unchanged.
CarryForwardUnchanged bool
// Enabled reports whether ANY consumer of the setting is live in this
// daemon, which is what decides if a saved value has an effect today.
//
// Two consumers read it, and only one is the loop. The point-in-time
// restore folds into the same store with the same setting, and it is wired
// whenever a baseline supervisor exists, which --baseline-trigger alone is
// enough to create. Reading liveness off the refresh interval alone told an
// operator running --baseline-trigger that nothing was live, and then a
// Restore hard-linked their files. For a setting whose whole purpose is
// taking consent about the on-disk representation, that is the one thing
// the panel must not do.
Enabled bool
// Scheduled reports the narrower fact: a periodic refresh loop is running
// (--baseline-refresh-interval was set). Separate from Enabled because the
// panel's copy has to distinguish "this applies to your restores but
// nothing is on a timer" from "this applies to nothing until a restart".
Scheduled bool
// TableDeltas is the daemon's --baseline-table-deltas (#1638, on by
// default). The card needs it to tell the truth with
// CarryForwardUnchanged OFF: table deltas publish a table that did not
// change by linking its previous file too, so "every table is written
// again" is only true when BOTH are off.
TableDeltas bool
}
BaselineRefreshDefaults is the daemon-side baseline-refresh configuration, surfaced to the settings panel as the fallback when nothing is saved.
type BaselineRefreshReporter ¶ added in v0.50.0
type BaselineRefreshReporter interface {
// RefreshStatus reports the latest periodic refresh for a server, or state
// "idle" when none has run here. Kept apart from BaselineController.Status
// because the two answer different questions — "did my dump work" and "is my
// baseline still moving forward on its own" — and one shared slot would let
// a manual dump erase the evidence that the automatic refresh has been
// failing for a week.
RefreshStatus(serverID string) BaselineStatus
}
BaselineRefreshReporter reports the daemon's periodic baseline refresh (#1171). Deliberately a SEPARATE interface from BaselineController, wired from a separate Config field, because the two features are independently opt-in: the manual dump needs mydumper and BINTRAIL_CONSOLE_BASELINE_TRIGGER=1, while a refresh needs neither — it exists precisely so a fresher baseline does not require a dump. Folding the report into BaselineController would gate the refresh behind the dump's opt-in, or (worse) un-gate the Create-baseline button for anyone who enabled only the refresh.
nil when the daemon runs no refresh loop.
type BaselineRequest ¶ added in v0.21.0
type BaselineRequest struct {
ServerID string
ServerName string
SourceDSN string
// SourceSSL is how the full read connects to the source, its pre-checks
// and mydumper alike: the entry's SourceSSL, the TLS capture uses (#1996).
SourceSSL config.SSL
Schemas []string
// LocalDir is the per-server baseline directory (entry.BaselineDir). When
// set, the snapshot is written there persistently and NOT uploaded. Empty
// means S3-only: stage in a temp dir, upload to S3, discard the staging.
LocalDir string
// S3 is the per-server baseline S3 prefix (entry.BaselineS3, s3://…). When
// set, the staged snapshot is uploaded there. Region/credentials come from
// the ambient AWS chain (env / ~/.aws / IAM role), like the rest of the
// console's S3 access.
S3 string
// Flavor selects the baseline producer: "postgres" runs internal/pgbaseline
// (COPY + the slot's pgoutput anchor); anything else runs mydumper. Plain
// strings keep internal/console pgx-free (the read-layer dependency guard).
Flavor string
// Slot / Publication are the PostgreSQL replication slot and publication
// (postgres flavor only); the producer stamps the snapshot at the slot's
// consistent-point LSN so Time-travel/reconstruct can replay deltas from it.
// Empty for MySQL/MariaDB.
Slot string
Publication string
// Trigger is stamped onto the run's history record:
// BaselineRunTriggerScheduled when the backup schedule started this job,
// empty for the Create backup button.
Trigger string
// Why is the reason the backup schedule chose a FULL backup for this
// run (#1604), persisted on the run record so the page can say what was
// missing; empty for a manual backup.
Why string
// IndexDSN is the server's index. The dump reads its high-water mark
// before it starts, so the update that follows this full backup is
// measured (#1737); empty means no mark is read.
IndexDSN string
}
BaselineRequest is the in-process job description the endpoint hands the controller. The source DSN (a secret) stays inside the process — it is never written to disk or serialized to any HTTP response.
func BaselineRequestFor ¶ added in v0.71.0
func BaselineRequestFor(e ServerEntry) BaselineRequest
BaselineRequestFor builds the in-process job description for a registry entry. One constructor for the button and the schedule, so the two can never dump a different schema set or destination for the same server.
type BaselineRestoreRequest ¶ added in v0.65.0
type BaselineRestoreRequest struct {
ServerID string
ServerName string
IndexDSN string
// BaselineDir is the server's own local directory: where the fold WRITES,
// and where it reads the backup to fold from when BaselineS3 is empty.
BaselineDir string
// BaselineS3 is the server's configured S3 destination, or empty. When
// set, the backup to fold from is looked for in the bucket (the
// BaselineFoldSource rule) and the finished snapshot is uploaded there, so the restore
// behaves like the scheduled update on the same server (#1541). Before it
// was carried, the restore listed the local directory alone, which on an
// S3-backed server holds only what this daemon folded since it started,
// and refused with "no snapshot exists" while the bucket held dozens.
BaselineS3 string
At time.Time
// CarryForwardUnchanged is the effective setting the console resolved for
// this restore. A restore is the same fold the refresh performs, into the
// same store, so it honours the same operator choice; leaving it out is
// how the two silently diverged.
CarryForwardUnchanged bool
}
BaselineRestoreRequest identifies the server and the instant to restore to.
type BaselineRestorer ¶ added in v0.65.0
type BaselineRestorer interface {
// TriggerRestore starts the restore asynchronously. ErrBaselineRunning
// when another baseline job for the server is in flight.
TriggerRestore(req BaselineRestoreRequest) error
// RestoreStatus reports the last restore for a server (idle if none).
RestoreStatus(serverID string) BaselineStatus
}
BaselineStatus is the pollable state of a server's most recent baseline job. BaselineRestorer runs an operator-chosen point-in-time restore: fold the snapshot at-or-before At forward through the index's deltas and publish the result as a NEW discoverable snapshot in the same baseline store (the backups page's PITR action). Deliberately a separate interface from BaselineController and BaselineRefreshReporter: the three features are independently wired, and deriving one from another has already produced a daemon that refused to start (#1171's lesson).
type BaselineRunHistory ¶ added in v0.65.0
type BaselineRunHistory struct {
// contains filtered or unexported fields
}
BaselineRunHistory is the persisted baseline-run history: one JSON file, capped per server, written atomically like the server registry and the verify history. Console-local state on disk, deliberately NOT a table in the index database (registry DSNs never receive DDL).
func OpenBaselineHistory ¶ added in v0.65.0
func OpenBaselineHistory(path string) (*BaselineRunHistory, error)
OpenBaselineHistory loads the history at path. A missing file is an empty history; a corrupt or newer-versioned file is an error for the caller to decide on, never silently truncated.
func (*BaselineRunHistory) Append ¶ added in v0.65.0
func (h *BaselineRunHistory) Append(rec BaselineRunRecord) error
func (*BaselineRunHistory) AppendCompact ¶ added in v0.84.0
func (h *BaselineRunHistory) AppendCompact(rec BaselineRunRecord) (bool, error)
AppendCompact records a table-delta compaction run (#1723). When the run failed and the newest record for the server is a compaction that failed the same way, that record's end moves to this run instead of a new record being added, for the reason AppendSkip gives: the job is retried at every refresh, so a persistent failure would otherwise append an identical record every cycle and push the refreshes off the capped history. A success, or a different error, is a new record. Returns whether a NEW record was added.
func (*BaselineRunHistory) AppendSkip ¶ added in v0.71.0
func (h *BaselineRunHistory) AppendSkip(rec BaselineRunRecord) (bool, error)
AppendSkip records a scheduled slot that did not start. When the newest record for the server is already the same skip, that record's FinishedAt moves to this slot instead of a new record being added: a wedged job plus a short interval would otherwise append an identical skip every slot and push the durations ledger out of the capped history, while a frozen timestamp would have the page report the FIRST missed slot of a streak as if the streak had ended there. Returns whether a NEW record was added.
func (*BaselineRunHistory) EmergencyStarted ¶ added in v0.95.0
func (h *BaselineRunHistory) EmergencyStarted(serverID string) string
EmergencyStarted is the newest emergency full read's start NoteEmergencyStart recorded for the server, "" if none.
func (*BaselineRunHistory) FindBySnapshot ¶ added in v0.65.0
func (h *BaselineRunHistory) FindBySnapshot(serverID, snapshotTime string) *BaselineRunRecord
FindBySnapshot returns the newest record for serverID whose SnapshotTime equals snapshotTime (RFC3339 UTC), or nil.
func (*BaselineRunHistory) FullBackupFinished ¶ added in v0.87.0
func (h *BaselineRunHistory) FullBackupFinished(serverID, snapshotTime string) time.Time
FullBackupFinished is when the full backup that published the snapshot named snapshotTime (RFC3339 UTC) finished, zero when no full backup on record published it or its stamp does not parse: what the age rule counts an anchor's age from (BackupWindow.AnchorFullFinished).
func (*BaselineRunHistory) IndexMarkFor ¶ added in v0.84.0
func (h *BaselineRunHistory) IndexMarkFor(serverID, snapshotTime string) (uint64, bool)
IndexMarkFor is the index high-water mark read before the successful update or full backup that published the snapshot named snapshotTime (RFC3339 UTC), and whether one is on record: the base an update from that snapshot counts its events from, when this daemon's in-memory memo does not name it (a restart emptied it, or the snapshot is a full backup's, which folds nothing and leaves no memo, #1737).
A full backup's record counts even with an error: it names a snapshot only when one was published, and an error beside that is the upload (or a shutdown during it). The next update reads that local copy (resolveFoldSource prefers it when it is ahead of the bucket), so its mark is the right base. An update's record counts only without an error. No other record counts (a skip names no snapshot at all).
func (*BaselineRunHistory) LastFullBackup ¶ added in v0.84.0
func (h *BaselineRunHistory) LastFullBackup(serverID string) time.Duration
LastFullBackup is how long the newest successful full backup for serverID took, start to finish (the upload included: that is what the schedule waits for); zero when there is none on record or its stamps do not parse.
func (*BaselineRunHistory) LastFullCopy ¶ added in v0.86.0
func (h *BaselineRunHistory) LastFullCopy(serverID string) (run, skip *BaselineRunRecord)
LastFullCopy returns the newest scheduled full backup the full-backup timetable started (#1564), whether it succeeded or not, and the newest of its slots that did not start, either nil.
func (*BaselineRunHistory) LastFullRead ¶ added in v0.86.0
func (h *BaselineRunHistory) LastFullRead(serverID string) *BaselineRunRecord
LastFullRead returns the newest full backup of the server that succeeded, whatever started it (the full-backup timetable, the automatic choice, the fallback for a failed update, a click), or nil. It is what ends the page's "the full read did not run" line (#1564): what the timetable asks for is a real read of the database, and any successful one after the miss is one.
func (*BaselineRunHistory) LastScheduled ¶ added in v0.71.0
func (h *BaselineRunHistory) LastScheduled(serverID string) (run, skip *BaselineRunRecord)
LastScheduled returns the newest scheduled RUN for serverID and the newest scheduled SKIP, either nil when there is none. Both, because they answer different questions on the Snapshots page: "when did the schedule last produce a backup" and "is it currently unable to". A skip newer than the last run is the case the page has to shout about.
func (*BaselineRunHistory) LastSourceRead ¶ added in v0.87.0
LastSourceRead is when the newest full backup that published a snapshot at or before atOrBefore STARTED, zero when there is none on record or its start does not parse: the instant the capture's dropped rows are dated against (#1791). A full backup reads the source, so a row the capture dropped before it is in its snapshot and in every update folded from it; an update reads only the index, which never received that row, so an update anchor proves nothing about it. The start, not the finish: the dump's consistent read is taken as it starts, and a row dropped while it ran is in neither. Only a full backup that succeeded counts: one that failed after publishing may have reached only one of its destinations (published here, the upload refused), and a fold reading the other one descends from an older full backup, which never read those rows. Both stamps come from this process's clock (the capture's skip ledger and the run record); a ledger written by a capture on another host is only as comparable as the two clocks.
func (*BaselineRunHistory) List ¶ added in v0.65.0
func (h *BaselineRunHistory) List(serverID string) []BaselineRunRecord
List returns a copy of serverID's records, oldest first.
func (*BaselineRunHistory) MeasuredSinceFull ¶ added in v0.87.0
func (h *BaselineRunHistory) MeasuredSinceFull(serverID string) bool
MeasuredSinceFull reports whether a measured successful update for serverID was recorded after its newest full backup (one that succeeded or published a snapshot, see below), or there is no full backup on record. False is the model deciding on evidence older than the last full backup, and CutoverToFull then lets the update run (#1737): a full backup chosen on a rate that no update after it has re-measured would choose the next one on the same rate, forever.
Record order, not timestamps: an update that ran while a full backup was still uploading (#1725) is recorded before it, and counts as before. That costs one more update after that full backup, never a missed one. A full backup counts when it succeeded or published a snapshot whose upload then failed: either way it read production and the next update folds from what it wrote. One that failed before publishing did not.
func (*BaselineRunHistory) NoteEmergencyStart ¶ added in v0.95.0
func (h *BaselineRunHistory) NoteEmergencyStart(serverID, at string) error
NoteEmergencyStart records, durably and before the read runs, that an emergency full read of the server started at (RFC 3339).
func (*BaselineRunHistory) Path ¶ added in v0.95.0
func (h *BaselineRunHistory) Path() string
Path is the file the history is kept in.
func (*BaselineRunHistory) ProvenUpdate ¶ added in v0.84.0
func (h *BaselineRunHistory) ProvenUpdate(serverID string, within time.Duration) int64
ProvenUpdate is the most events any of the newest measuredFoldRuns measured successful updates for serverID applied in less than within: what the history proves an update can do cheaper than that. The same sample the model fits (#1736): a fast day months back is not evidence about this disk today, and the margin CutoverToFull allows past the proven size makes stale evidence reach further. Zero when nothing in the sample qualifies.
func (*BaselineRunHistory) UpdateModel ¶ added in v0.84.0
func (h *BaselineRunHistory) UpdateModel(serverID string) (fixed time.Duration, rate float64)
UpdateModel fits what an update costs for serverID from its last few measured successful updates: fixed seconds every update pays, taken as the shortest run in the sample, plus a rate in events per second from what the OTHER runs applied and took BEYOND that run (its events are paid for inside the fixed cost, so counting them again would read the rate too fast and the estimate too cheap). A run that took longer but applied no more events (a slow disk that day) says nothing about the per-event cost and is left out. The rate is zero (unknown) with fewer than two samples, or when the time beyond the fixed cost is under a tenth of the sample's total: then every update cost about the same whatever it applied, the per-event cost is not distinguishable, and a rate read off such runs would be a small number that makes any burst look like hours. Two more ways to no rate (#1736). When the events beyond the shortest run are under a tenth of a typical run's (the sample's mean), every update applied about the same however long it took (a steady load, where the durations differ by noise), the slope has no lever arm, and the rate is unknown too; the scale is a run's size, not the sample's total, so the guard does not loosen as the sample fills. And when the marginal rate comes out under a tenth of the shortest run's whole rate (its events over all its seconds, a floor no true per-event rate is below), it is noise in the denominator whatever the spread. And when no run in the sample applied at least measuredFoldMinEvents more than the shortest (#1738): on a quiet server every relative guard is passed by one update that applied a hundred events more and took a noisy fifty seconds longer, because the server's own scale is that small. Not a whole-run rate in place of the unknown one: on a loaded server that would be near the truth, on a quiet one the fixed cost divided by a handful of events, and the model cannot tell the two apart. Refusing a real rate costs at most one update that runs longer than a full backup would have (with no load on the source), and that update is the wide sample the next fit reads the rate from; believing an artefact costs a full read of production at every burst.
func (*BaselineRunHistory) UpdateSample ¶ added in v0.87.0
func (h *BaselineRunHistory) UpdateSample(serverID string) (n int, newest time.Time)
UpdateSample is how many measured successful updates the model fits for serverID (at most measuredFoldRuns) and when the newest of them finished; zero time when there is none or its stamp does not parse. What the daemon log says next to a full backup the model chose (#1737), so a rate that stopped being re-measured shows its age there.
type BaselineRunRecord ¶ added in v0.65.0
type BaselineRunRecord struct {
ServerID string `json:"server_id"`
ServerName string `json:"server_name,omitempty"`
Kind string `json:"kind"`
// Trigger is BaselineRunTriggerScheduled for the backup schedule's own
// runs and skips, empty otherwise.
Trigger string `json:"trigger,omitempty"`
// SkipReason is set on a scheduled slot that did not start; see above.
SkipReason string `json:"skip_reason,omitempty"`
// SnapshotTime is the published snapshot's anchor instant — its directory
// name — in RFC3339 UTC. Empty when the run failed before publishing, or
// when the producer does not report it (PostgreSQL dumps stamp the
// snapshot server-side, out of this process's sight).
SnapshotTime string `json:"snapshot_time,omitempty"`
StartedAt string `json:"started_at"`
FinishedAt string `json:"finished_at"`
Tables int `json:"tables,omitempty"`
// ViewsSkipped: see BaselineStatus.ViewsSkipped. Zero also for every run
// recorded before the count existed, so zero is never shown as a count.
ViewsSkipped int `json:"views_skipped,omitempty"`
// LeftOutTables / LeftOutTablesOmitted: see BaselineStatus. Kept with the
// run: the new-tables check reads them to know these tables are not new.
LeftOutTables []RefusedTable `json:"left_out_tables,omitempty"`
LeftOutTablesOmitted int `json:"left_out_tables_omitted,omitempty"`
// LeftOutKeys is every left-out table as the source names it (see
// LeftOutKeys), uncapped: what the new-tables check leaves alone.
LeftOutKeys []string `json:"left_out_keys,omitempty"`
// Carried counts tables published by reusing the previous snapshot's file
// (refresh and restore). Persisted rather than left to the live status,
// which the next run overwrites: whether a run cost a full rewrite is
// exactly the thing an operator looks back at when sizing a disk or an
// interval, and by then the live status is gone.
Carried int `json:"carried,omitempty"`
// CarriedCopied narrows Carried to the reuses published as full byte
// copies (no hard link, no disk saved) — see BaselineStatus.CarriedCopied.
CarriedCopied int `json:"carried_copied,omitempty"`
Rows int64 `json:"rows,omitempty"`
Uploaded int `json:"uploaded,omitempty"`
Refused int `json:"refused,omitempty"`
// RefusedTables names the tables behind Refused, up to RefusedTablesCap,
// and RefusedTablesOmitted counts the rest (#1653). A run recorded before
// the list existed has the count and no list, which is "not recorded",
// never "none refused". New keys, same file version: a version of this
// program from before them reads the file and ignores them.
RefusedTables []RefusedTable `json:"refused_tables,omitempty"`
RefusedTablesOmitted int `json:"refused_tables_omitted,omitempty"`
// NewTables / NewTablesOmitted / NewTablesUnchecked: see BaselineStatus
// (#1993). Kept with the run: the live status is overwritten by the next.
NewTables []string `json:"new_tables,omitempty"`
NewTablesOmitted int `json:"new_tables_omitted,omitempty"`
NewTablesUnchecked string `json:"new_tables_unchecked,omitempty"`
NewTablesAction string `json:"new_tables_action,omitempty"`
NewTablesActionReason string `json:"new_tables_action_reason,omitempty"`
Error string `json:"error,omitempty"`
// Failure: see BaselineStatus.Failure. Absent in records written before
// #1986, which the page draws as the generic card.
Failure *SnapshotFailure `json:"failure,omitempty"`
// Why is the reason a scheduled run was a FULL backup rather than an
// update, as decided when it ran (#1604); WhyCode is BackupWhyCode of
// it, fixed at write time. Empty on updates and manual backups.
Why string `json:"why,omitempty"`
WhyCode string `json:"why_code,omitempty"`
// DiskCheck / DiskNote: see BaselineStatus. Kept with the run so a read
// that ran on a low disk says so after the live status is gone.
DiskCheck string `json:"disk_check,omitempty"`
DiskNote string `json:"disk_note,omitempty"`
// TransportNote: see BaselineStatus. A read made in cleartext says so
// after the live status is gone.
TransportNote string `json:"transport_note,omitempty"`
// Events is how far the index high-water mark moved between this
// daemon's previous fold of the snapshot an update started from and
// this one (every source writing to the index counts, and so do rows a
// resumed capture re-inserted), UpdateSeconds how long the whole update
// run took, upload included, like the full backup duration it is
// compared against (#1721), and IndexMark the high-water mark read
// before the fold: the base the NEXT update's Events are counted from,
// kept here so the count survives a daemon restart. A MySQL full
// backup that published a snapshot records IndexMark too, read before
// its dump started, so the update that follows it is measured like any
// other (#1737); it has no Events or UpdateSeconds. A PostgreSQL one
// cannot: its snapshot instant is stamped by the database, so no record
// names it, and the update after it goes unmeasured (the one after that
// is measured from the daemon's memo). Zero when not measured (a
// restore, a fold with no previous mark, an index that did not answer).
Events int64 `json:"events,omitempty"`
UpdateSeconds float64 `json:"update_seconds,omitempty"`
IndexMark uint64 `json:"index_mark,omitempty"`
}
BaselineRunRecord is one completed baseline-producing run as this daemon performed it. The files listing joins it to a snapshot by SnapshotTime to report the run's exact duration; snapshots produced elsewhere (the CLI, another daemon) have no record and fall back to the file write span.
A record with SkipReason set is NOT a run: it is a scheduled slot that could not start (another backup job held the server, or the schedule was not runnable). It has no snapshot, so the files listing never joins it; it exists so a schedule that never gets to run stays visible on the Snapshots page instead of silent.
type BaselineStatus ¶ added in v0.21.0
type BaselineStatus struct {
// State: idle | running | succeeded | failed, plus two terminal states
// only a sql-export build reaches once its staged files are gone —
// downloaded (the archive reached a client) and expired (the download
// deadline passed, or the files were removed from under it).
State string `json:"state"`
Since string `json:"since,omitempty"`
FinishedAt string `json:"finished_at,omitempty"`
LastError string `json:"last_error,omitempty"`
// Failure is why a full read failed, as data, when it is a cause the page
// can explain in plain words (#1986); nil otherwise and on success.
Failure *SnapshotFailure `json:"failure,omitempty"`
// Published: this run left a complete snapshot in the server's local
// directory. Normally that is just State == "succeeded", but the two
// diverge on one failure — the fold finished and marked the snapshot and
// only the upload to the backup destination failed (#1539) — and that is
// exactly the case where asking "did it fail?" gives the wrong answer to
// "is a snapshot still owed?".
Published bool `json:"published,omitempty"`
// Uploading: the snapshot is published locally and its copy to the backup
// destination is still in flight (#1725). The server's job slot is
// already free; Uploaded is filled in when the copy completes.
Uploading bool `json:"uploading,omitempty"`
// Swept counts OTHER local snapshots a full backup sent to the destination
// because the destination lacked them (a refresh whose upload failed).
Swept int `json:"swept,omitempty"`
Tables int `json:"tables,omitempty"`
// ViewsSkipped counts the views the source held when it was read in full.
// A view has no rows of its own, so it is not in the snapshot; the daemon
// log names each one.
ViewsSkipped int `json:"views_skipped,omitempty"`
// LeftOutTables names the tables a full read left out of its snapshot
// because their real name cannot be read back from the dump or stored
// (#2006), up to RefusedTablesCap, with why (Verdict "left-out");
// LeftOutTablesOmitted counts the rest, and every name withheld from a
// session with a data profile.
LeftOutTables []RefusedTable `json:"left_out_tables,omitempty"`
LeftOutTablesOmitted int `json:"left_out_tables_omitted,omitempty"`
// Carried counts tables published by reusing the previous snapshot's file
// rather than folding them again (refresh and restore only). It is the
// ONLY confirmation the operator gets that the reuse setting did anything:
// without it a run that reused every file and a run that rewrote every
// file report identically.
Carried int `json:"carried,omitempty"`
// CarriedCopied narrows Carried to the reuses that fell back to a full
// byte copy (no hard link, so no disk saved). Without the split the UI
// confirmed a disk saving the daemon log denied (#1578).
CarriedCopied int `json:"carried_copied,omitempty"`
Rows int64 `json:"rows,omitempty"`
// Bytes is the finished artifact's on-disk weight (sql-export builds
// only) — the UI's download confirm and Ready line read it.
Bytes int64 `json:"bytes,omitempty"`
Uploaded int `json:"uploaded,omitempty"`
// At is the anchor instant of a refresh or restore run (RFC3339 UTC): the
// moment the published snapshot represents, which is also its directory
// name. A sql-export build stamps it too (the instant the dump
// represents; it publishes no snapshot). Empty on dump jobs (the anchor
// is chosen mid-run).
At string `json:"at,omitempty"`
// CheckedAt (periodic refresh only, RFC3339 UTC) is when a refresh cycle
// for this server last ended, whether it produced a snapshot or found
// nothing to fold. A cycle with nothing to fold writes no status and no
// run record, so without this a quiet server and a refresh loop that
// stopped report the same last run. It describes the loop, not the run:
// every other field here still belongs to the last run that happened.
CheckedAt string `json:"checked_at,omitempty"`
// ExpiresAt (sql-export builds only, RFC3339 UTC) is when a finished
// build is removed from the daemon's disk unless downloaded first; the
// Snapshots page shows it as the download deadline.
ExpiresAt string `json:"expires_at,omitempty"`
// DownloadedAt (sql-export builds only, RFC3339 UTC) stamps the
// download that consumed the build.
DownloadedAt string `json:"downloaded_at,omitempty"`
// StagingError (sql-export builds only) says why the staged files are
// still on disk when they should not be (a removal that failed and is
// retried every minute) or why they could not be read. Empty when fine.
StagingError string `json:"staging_error,omitempty"`
// RemovalOwed (sql-export builds only) is true while THIS build's own
// removal has been decided and has not succeeded yet: its files are
// still on disk but it is no longer downloadable. A StagingError about
// a previous build (one this build's start could not clear) leaves it
// false, and the build stays downloadable.
RemovalOwed bool `json:"removal_owed,omitempty"`
// Refused counts tables a refresh declined to fold (gap / schema change).
// A refresh that refuses every table is not a failure of the daemon — it is
// a correct fail-closed verdict — so it reports succeeded=false with this
// count rather than an opaque error.
Refused int `json:"refused,omitempty"`
// RefusedTables names the tables behind Refused, each with its verdict and
// reason (#1653), up to RefusedTablesCap; RefusedTablesOmitted counts the
// ones past the cap. Both are empty on a run whose failure was not a
// table's (no snapshot to start from, an index that did not answer), and
// for a session with a data profile. Refused stays the count either way.
RefusedTables []RefusedTable `json:"refused_tables,omitempty"`
RefusedTablesOmitted int `json:"refused_tables_omitted,omitempty"`
// NewTables (#1993, refresh only) names the tables in the source's scope
// that the PUBLISHED snapshot does not hold, because they were created
// after the snapshot the update started from: an update folds changes
// onto the tables it already has, so a new table has nothing to fold
// onto. Up to RefusedTablesCap names; NewTablesOmitted counts the rest.
// Empty on a run that published nothing (its fallback full read holds
// every table) and for a session with a data profile.
//
// NewTablesUnchecked says why the source could not be asked for its
// tables. A non-empty value means "not known", never "none": an empty
// list beside it is not a finding.
NewTables []string `json:"new_tables,omitempty"`
NewTablesOmitted int `json:"new_tables_omitted,omitempty"`
NewTablesUnchecked string `json:"new_tables_unchecked,omitempty"`
// NewTablesAction is what the daemon decided to do about NewTables, so
// the page never promises a full read that is not coming (one of the
// NewTablesAction* values; empty on a record from before it, or on an
// update whose snapshot did not reach its destination).
// NewTablesActionReason carries the gate's refusal for
// NewTablesActionNotPossible.
NewTablesAction string `json:"new_tables_action,omitempty"`
NewTablesActionReason string `json:"new_tables_action_reason,omitempty"`
// NewTablesSnapshot (live status only, RFC3339) is the snapshot the list
// describes: the last update that published. A later update that fails
// keeps the list, because the newest copy still lacks those tables.
NewTablesSnapshot string `json:"new_tables_snapshot,omitempty"`
// TooManyChanges: the run refused for too many changed rows
// (reconstruct.ErrTouchedRowBudget, #1107): one of a table's on-disk groups
// of changes passed the per-table limit, or a table with no backup passed
// the limit itself. The next automatic
// refresh starts from the same backup over a longer window and would be
// refused again, so the page asks for a full backup instead of promising
// a retry.
TooManyChanges bool `json:"too_many_changes,omitempty"`
// DiskRefused: the run stopped for disk space (#1614), refused before a
// file it could not hold, or a write that found the disk full. A scheduled update refused this way must NOT
// fall back to a full backup: that one writes into the same directory.
DiskRefused bool `json:"disk_refused,omitempty"`
// DiskCheck and DiskNote (full reads of MySQL/MariaDB only, #1938): the
// free-space check taken before mydumper starts. DiskCheck is "ok",
// "low" (room for the dump, maybe not for the dump plus its Parquet copy;
// the read ran anyway) or "unchecked" (the check could not run, and the
// read went ahead). DiskNote is the sentence to show. A refusal is a
// failed run with DiskRefused set and the reason in LastError, and
// carries neither. Set while the read is still running.
DiskCheck string `json:"disk_check,omitempty"`
DiskNote string `json:"disk_note,omitempty"`
// TransportNote (full reads of MySQL/MariaDB only, #1996): set when the
// read reached the source WITHOUT encryption, saying why ("read without
// encryption: the source offers no TLS (TLS mode preferred)"); empty
// for an encrypted read. Set while the read is still running.
TransportNote string `json:"transport_note,omitempty"`
// ForeignSource: the job was refused because the snapshot it would build
// on was written by another writer, or its writer could not be told
// (#1684). The schedule does not answer it with a full read: that would
// publish into the same shared location.
ForeignSource bool `json:"foreign_source,omitempty"`
}
type CaptureStatus ¶ added in v0.91.0
type CaptureStatus struct {
// ServerID is the server the answer is about, so the page can drop an
// answer that is not for the server on screen.
ServerID string `json:"server_id"`
State string `json:"state"`
// Detail says why the state is unknown or behind. No DSN, no error text.
Detail string `json:"detail,omitempty"`
// RetryInSeconds is set when the source looked ahead on a first read:
// asking again after this long settles it.
RetryInSeconds int `json:"retry_in_seconds,omitempty"`
}
CaptureStatus is the answer of GET /api/capture-status.
type CaptureStatusReporter ¶ added in v0.91.0
type CaptureStatusReporter interface {
// CaptureStatus answers for e. e.ID is the boot id for the daemon's own
// index, whose source the reporter knows.
CaptureStatus(ctx context.Context, e ServerEntry) CaptureStatus
}
CaptureStatusReporter asks a server's source whether capture is caught up. nil when this process is not connected to any source (the read-only web interface), where the answer is always unknown.
type CommandLineWriter ¶ added in v0.92.0
CommandLineWriter is the command-line server's location when this process writes snapshots for it; zero when it writes none.
type Config ¶
type Config struct {
// DB is the boot index database, from the command-line DSN. May be nil
// when the console is registry-only (servers come from Registry) or in
// unit tests that exercise only middleware/asset routes.
DB *sql.DB
// DBName is the boot index database name (from the DSN), used by the
// query planner and status collection.
DBName string
// BootDSN is the boot entry's DSN, used ONLY to render the masked
// host/user/dbname view in /api/servers. Optional.
BootDSN string
// HideBoot removes the boot entry from the UI entirely (selector, server
// list, default selection). Set by source-less `bintrail-console watch`:
// its boot index is only the control plane's anchor database — no stream
// ever writes to it — and surfacing it as a "server" made fresh installs
// look pre-populated. Header-less requests still resolve to the boot
// bundle underneath, so the views render before the first server exists.
HideBoot bool
// BootCaptureFilter is the scope THIS process's boot capture runs with:
// `bintrail-console watch --schemas/--tables`. It is the only place that
// scope exists — the schema snapshot records the schemas, nothing records
// the per-table filter, and the parser drops a filtered-out table's events
// with no counter and no log line. Without it the uncaptured-tables report
// would count the snapshot and state full coverage over tables nobody is
// watching (#1802). nil means this process does not run the boot capture,
// and the report then claims no count at all.
BootCaptureFilter *status.CaptureFilter
// BootSourceFlavor reports the flavor THIS process's boot capture runs as
// ("mysql" or "mariadb"; "" until capture has asked its source). The boot
// entry has no saved Source type, so the server list labels it with this.
// nil means this process runs no boot capture.
BootSourceFlavor func() string
// Registry is the named-server store (a local YAML file — the only thing
// the console ever writes). nil means an empty in-memory registry.
Registry *Registry
// Listen is the bind address (host:port). Default 127.0.0.1:8090.
Listen string
// Token is an OPT-IN static automation credential — set it explicitly for
// scripts/curl. It is never generated. Password login is the primary path:
// with no token and no password, a loopback bind (or AllowSetup) enters
// first-run setup, and a non-loopback bind is refused.
Token string
// NoArchive disables Parquet archive auto-discovery on the boot entry.
// The caller forces this true when a profile is active (archives do not
// enforce RBAC). Registry entries carry their own per-server flag.
NoArchive bool
// DenyTables and RedactColumns are the resolved profile RBAC rules,
// applied to every query the console runs — on EVERY server.
DenyTables []query.SchemaTable
RedactColumns []query.SchemaTableColumn
// ProfileActive is set by the caller whenever a profile NAME was supplied,
// even if it resolved to ZERO deny/redact rules (an empty profile). It forces
// query.Options.ProfileActive on every query so QueryText/QueryHash are
// withheld under EVERY named profile (#699), and — since Parquet archives do
// not run the redaction pass — couples with NoArchive so archive rows cannot
// leak that statement text either (#838). Independent of the rule count,
// which stays the signal for rbacActive() (cascade/reconstruct gating).
ProfileActive bool
// AllowedHosts is an optional allowlist of hostnames accepted in the Host
// header (in addition to IP literals and localhost), for operators who
// front the console with a DNS name.
AllowedHosts []string
// MonitorCtrl is the control-plane supervisor, wired in ONLY by
// `bintrail-console watch` (the write-capable daemon). nil on the
// standalone read-only console: /api/capabilities reports monitor:false
// there and every monitor verb refuses at the endpoint with 403,
// mirroring how reconstruct gates on baselineConfigured.
MonitorCtrl MonitorController
// BaselineCtrl runs in-process baseline snapshots (dump→convert→upload) for
// a monitored server (#613). Wired in ONLY by `bintrail-console watch` when
// the operator opts in (BINTRAIL_CONSOLE_BASELINE_TRIGGER=1); nil otherwise,
// where the trigger endpoint refuses with 403 and /api/capabilities reports
// baseline_trigger:false. Set together with MonitorCtrl (both control-plane).
BaselineCtrl BaselineController
// BaselineRefresh reports the daemon's PERIODIC baseline refresh (#1171).
// Wired in ONLY by `bintrail-console watch --baseline-refresh-interval`, and
// deliberately a separate field from BaselineCtrl because the two features
// are independently opt-in — the refresh needs no mydumper and no
// BINTRAIL_CONSOLE_BASELINE_TRIGGER=1. nil when no refresh loop runs, and the
// baseline listing then simply carries no refresh section.
BaselineRefresh BaselineRefreshReporter
// BaselineRestore runs operator-chosen point-in-time restores into the
// server's own baseline store. Wired whenever the watch daemon has a
// baseline supervisor (creation or refresh opt-in); nil elsewhere.
BaselineRestore BaselineRestorer
// BackupSchedules is the watch daemon's per-server backup schedule loop
// (#1442). Wired whenever the daemon has a baseline supervisor; nil
// elsewhere, where the schedule endpoints refuse with 403 and the listing
// reports a saved schedule as not runnable.
BackupSchedules BackupScheduleReporter
// CaptureStatus asks a server's source whether capture has read
// everything it wrote (#1794). Wired in ONLY by `bintrail-console watch`,
// the process that is connected to the sources; nil elsewhere, where
// GET /api/capture-status answers unknown.
CaptureStatus CaptureStatusReporter
// SQLExport builds custom .sql backups (fold at a chosen instant,
// download as a mydumper-format dump). Wired with BaselineRestore.
SQLExport SQLExporter
// BaselineHistory is the persisted record of baseline runs this daemon
// performed (dump/refresh/restore), used by the backups detail surface to
// report exact durations. Nil = no history (exact durations unavailable).
BaselineHistory *BaselineRunHistory
// SchemaSnapshotCtrl re-reads a monitored source's column layout and
// restarts that server's stream onto the result (#1296). Wired in by
// `bintrail-console watch` alongside MonitorCtrl and needing no separate
// opt-in — unlike BaselineCtrl it starts no dump and copies no row data.
// nil elsewhere, where the endpoints refuse with 403 and
// /api/capabilities reports schema_snapshot_trigger:false.
SchemaSnapshotCtrl SchemaSnapshotController
// VerifyCtrl runs bintrail verify's engine in-process for a monitored
// server (#677). Wired in ONLY by `bintrail-console watch` when the
// operator opts in (BINTRAIL_CONSOLE_VERIFY_TRIGGER=1); nil otherwise,
// where the trigger endpoint refuses with 403 and /api/capabilities reports
// verify_trigger:false. Set together with MonitorCtrl (both control-plane).
VerifyCtrl VerifyController
// VerifyHistory is the persisted verify-run history store (#1191), wired
// in by `bintrail-console watch` together with VerifyCtrl. nil elsewhere;
// the history endpoint then refuses with 403 like the other verify verbs.
VerifyHistory *VerifyHistory
// Telemetry is the live usage-telemetry client, wired in by
// `bintrail-console watch` so the UI opt-out toggle stops the running
// daemon's beacons immediately. nil on the read-only console (serve), where
// the toggle still persists the machine-wide choice to the consent file.
Telemetry TelemetryController
// SQLLimits are the caps free SQL over the copy runs under (#1952):
// threads, memory, wall clock, rows. Zero fields take
// sqlsandbox.DefaultLimits (2 threads, 2 GB, 60 s, 1,000 rows).
SQLLimits sqlsandbox.Limits
// BaselineDir / BaselineS3 enable point-in-time reconstruct (Phase 2) on
// the boot entry. When either is set (and no RBAC profile is active), the
// "Reconstruct" surface is exposed. BaselineDir takes precedence;
// BaselineS3 is an s3:// prefix. Registry entries carry their own.
BaselineDir string
BaselineS3 string
// AuthPath locates the console credential file (username + bcrypt hash,
// written by `bintrail-console user set-password`). Empty means
// DefaultAuthPath(). A missing file means password login is not
// configured; a corrupt one fails New loudly.
AuthPath string
// MCPTokenPath locates the managed MCP token file (SHA-256 only, written
// by the Settings → MCP Server generate flow — #1052). Empty means
// DefaultMCPTokenPath(). A missing file means no managed token; a corrupt
// one is logged loudly and disables the managed token until regenerated —
// unlike AuthPath, it deliberately never fails New (the daemon may be the
// stream supervisor).
MCPTokenPath string
// TLSCert / TLSKey serve the console over HTTPS (both-or-neither). Static
// files only — rotation is a restart; ACME is out of scope.
TLSCert string
TLSKey string
// AllowSetup permits browser first-run password setup on a NON-loopback
// bind — an assertion that the bind is access-controlled by other means.
// The compose stack sets it because it binds 0.0.0.0 inside the container
// but publishes the port on the host's loopback only; the container can't
// see that host mapping, so the operator asserts it. Loopback binds always
// allow setup regardless. Off by default: an unguarded setup endpoint on a
// truly public bind would let the first stranger claim the password.
AllowSetup bool
// RotationDefaults carries the daemon's --rotate-* flag/env values so
// GET /api/rotation can report the effective policy (and the console panel
// prefill it) before any console override is saved. Set by the watch
// daemon; zero on the standalone serve (which runs no rotation loop and
// hides the panel).
RotationDefaults RotationDefaults
// MayCreateFolders is set by the watch daemon (#1681): saving a server's
// snapshot folder may create it and write a check file into it, and a new
// server gets a default folder. The read-only serve leaves it false and
// only checks that a folder exists and is writable: it writes nothing on
// the filesystem but its registry.
MayCreateFolders bool
// LocalPruneLoop is set by the watch daemon when it runs the loop that
// removes local snapshots past a server's keep-newest count (#1681). The
// read-only serve leaves it false: nothing there removes anything, so the
// page must not say a retention is in force.
LocalPruneLoop bool
// BaselineRefreshDefaults carries the daemon's baseline-refresh flag/env
// values, the fallback the settings panel reports when no console override
// is saved. Same role as RotationDefaults above.
BaselineRefreshDefaults BaselineRefreshDefaults
// BackupSettingsDefaults carries the daemon-wide backup/snapshot flag and
// env values the Snapshots page (#1582) reports
// read-only: what the process was told, verbatim, with the exact name to
// change each one under. Same role as RotationDefaults above.
BackupSettingsDefaults BackupSettingsDefaults
// Version is the running build's version string ("0.36.0", or "dev" on
// unversioned builds), reported in /api/capabilities so the frontend can
// link release artifacts (the .mcpb bundle) matching the running binary.
// Optional; empty reads as an unversioned build.
Version string
// FlashbackListen is the address the embedded MySQL-protocol time-travel
// port (#996) is bound on, reported by GET /api/flashback so the Connect
// page can show how to reach it (#1446). Set ONLY by `bintrail-console
// watch --flashback-listen`, which refuses to start when that bind fails,
// so a non-empty value is an address that was bound at startup. It is
// NOT a liveness signal: a mid-run serve failure on that port is logged
// and swallowed (startFlashbackPort), and nothing here re-probes it.
// Empty on the standalone serve (which owns no such port) and on a watch
// that did not opt in; the page then reports the port as off. Display
// only: the console never opens or closes the port itself.
FlashbackListen string
}
Config configures a console Server. The caller (cmd/bintrail/console.go) is responsible for connecting the boot DB (from --index-dsn), running EnsureSchema on it, and resolving the profile's RBAC rules before constructing the server. Registry servers are connected lazily by the console itself — and never schema-migrated (see connManager).
type ConnectDraft ¶ added in v0.87.0
type ConnectDraft struct {
// Name is the name somebody TYPED, empty when they typed none. An
// automatic name is never stored here: restored into the form it would be
// sent back as typed, and refused as a duplicate as soon as another server
// held it. It is worked out again, under the registry's lock, on the next
// check.
Name string `yaml:"name,omitempty" json:"name"`
Flavor string `yaml:"flavor,omitempty" json:"flavor"`
SourceHost string `yaml:"source_host,omitempty" json:"source_host"`
SourcePort string `yaml:"source_port,omitempty" json:"source_port"`
SourceUser string `yaml:"source_user,omitempty" json:"source_user"`
Schemas string `yaml:"schemas,omitempty" json:"schemas"`
SourceDatabase string `yaml:"source_database,omitempty" json:"source_database"`
SourceSlot string `yaml:"source_slot,omitempty" json:"source_slot"`
SourcePublication string `yaml:"source_publication,omitempty" json:"source_publication"`
// Identified is what step 1 of the Connect screen found at SourceHost and
// SourcePort (#1953), kept so a reload in step 2 or 3 comes back there
// without probing the server again: a probe may count against DBTrail's
// address on the server, so a page load must never cause one.
Identified *DraftIdentity `yaml:"identified,omitempty" json:"identified,omitempty"`
// SavedAt is when this draft was last written (RFC3339), so a screen can
// say how old it is before offering to restore it.
SavedAt string `yaml:"saved_at,omitempty" json:"saved_at"`
}
ConnectDraft is the half-filled Connect form, kept between the moment somebody starts filling it in and the moment capture starts.
It exists because of one real interruption: the form shows a block of SQL to run on the database, and running it means leaving this page. Coming back to an empty form means typing it all again.
It never holds the database password (#1804). This is ONE form, shared by every session allowed to add servers (a session limited by a data profile included), so a stored password could be read by somebody it was never typed for. The screen keeps the password in the page only and asks for it again after a reload. A draft is not a server: nothing lists it, nothing connects with it, and it is thrown away the moment the server it describes starts capturing, or somebody discards it.
type DoctorCheck ¶
type DoctorCheck struct {
Name string `json:"name"`
Status string `json:"status"`
Detail string `json:"detail,omitempty"`
Remediation string `json:"remediation,omitempty"`
// Kind, Subjects and Statements are the doctor's typed finding (#1803):
// a fixed word a screen switches on, the things it names (privileges,
// tables, a setting), and for tables without a key one statement each.
// See internal/doctor/kind.go for the set. Empty on a pass.
Kind string `json:"kind,omitempty"`
Subjects []string `json:"subjects,omitempty"`
Statements []string `json:"statements,omitempty"`
// Light is the question the Connect screen's step 3 draws this check
// under (doctor.Lights: reach, login, rows, permissions, keys, other).
Light string `json:"light,omitempty"`
// Optional marks a warn about something capture works fine without
// (doctor.CheckResult.Optional). The screens fold it under "Optional
// improvements", outside the warning count.
Optional bool `json:"optional,omitempty"`
}
DoctorCheck is one preflight check result, JSON-shaped for the UI's remediation cards. Status is pass|fail|warn|skip.
type DoctorReport ¶
type DoctorReport struct {
Checks []DoctorCheck `json:"checks"`
Passed int `json:"passed"`
Failed int `json:"failed"`
Warnings int `json:"warnings"`
Skipped int `json:"skipped"`
// Optional counts the optional warns, which are NOT in Warnings: a start
// with only these is not a start "with warnings".
Optional int `json:"optional"`
}
DoctorReport aggregates the preflight checks for one source.
type DraftIdentity ¶ added in v0.92.0
type DraftIdentity struct {
Version string `yaml:"version,omitempty" json:"version,omitempty"`
Flavor string `yaml:"flavor,omitempty" json:"flavor,omitempty"`
Managed string `yaml:"managed,omitempty" json:"managed,omitempty"`
Proxy string `yaml:"proxy,omitempty" json:"proxy,omitempty"`
ServerError int `yaml:"server_error,omitempty" json:"server_error,omitempty"`
// ManagedChoice is the Connect screen's RDS/Aurora box when the person
// set it, apart from Managed (what the host name showed): nil when they
// left it as detected.
ManagedChoice *bool `yaml:"managed_choice,omitempty" json:"managed_choice,omitempty"`
}
DraftIdentity is the part of doctor.Identification the Connect screen needs to draw step 1's answer and step 2's block again.
type DraftStore ¶ added in v0.87.0
type DraftStore struct {
// contains filtered or unexported fields
}
DraftStore holds the one Connect draft. One, not a set: two people connecting two databases at the same moment share it, the same way they share the registry they are both writing to. Sharing it is why it holds no password (see ConnectDraft).
func NewDraftStore ¶ added in v0.87.0
func NewDraftStore(path string) *DraftStore
NewDraftStore returns the store for path. A path of "" never touches disk.
func (*DraftStore) Discard ¶ added in v0.87.0
func (d *DraftStore) Discard() error
Discard throws the draft away. Discarding one that is not there is not an error: it is what a second press of the button does, and what the successful end of Connect does after a reload already cleared it.
func (*DraftStore) Load ¶ added in v0.87.0
func (d *DraftStore) Load() (ConnectDraft, bool, error)
Load returns the saved draft. A missing or empty file is "no draft", not an error — an empty file is what a crash between create and write leaves, and restoring a form of blanks over what somebody is typing would be worse than restoring nothing.
A file that does not parse IS an error, deliberately: reporting it as "no draft" would send somebody to type everything again while what they typed sits on disk.
func (*DraftStore) Save ¶ added in v0.87.0
func (d *DraftStore) Save(c ConnectDraft) error
Save replaces the draft. The write is atomic and the file is 0600 inside a 0700 directory, the same discipline as the server registry: it names a database, its user and the addresses around it.
type FirstRunReport ¶ added in v0.82.0
type FirstRunReport struct {
Complete bool `json:"complete"`
Steps []FirstRunStep `json:"steps"`
CheckError string `json:"check_error,omitempty"`
}
FirstRunReport is GET /api/servers/{id}/first-run. Complete: the list is no longer shown, which is when a SNAPSHOT exists for the server and no capture step has failed (#1801). The list used to end at the first captured change and take its backup step with it, so from then on nothing on the Overview mentioned backups.
A captured change is deliberately NOT required: seeing your first change is a step someone may skip, because nobody can be made to write to production to get on, so a person who takes the snapshot instead has finished. The backup step's own state is what says a snapshot exists, and a failure on either side keeps the list up.
type FirstRunStep ¶ added in v0.82.0
type FirstRunStep struct {
Name string `json:"name"`
State string `json:"state"`
Detail string `json:"detail,omitempty"`
Fix string `json:"fix,omitempty"`
// SnapshotFailed marks the snapshot step failed by its last run (#1986):
// the page draws the snapshot failure card for it, with Detail inside its
// "Technical details" fold. Failure is why, when the cause is one the
// card can explain; nil draws the generic card.
SnapshotFailed bool `json:"snapshot_failed,omitempty"`
Failure *SnapshotFailure `json:"failure,omitempty"`
// Note is a line the snapshot failure card keeps visible: why the backup
// locations could not be checked, when they could not.
Note string `json:"note,omitempty"`
}
FirstRunStep is one row of the first-run list.
type FlashbackTarget ¶ added in v0.31.0
type FlashbackTarget struct {
// IndexDB is the open connection to the server's per-source index. It is
// owned by the connManager — the serving layer must not Close it.
IndexDB *sql.DB
// IndexDBName is the schema where binlog_events lives (the query planner
// scopes information_schema.PARTITIONS to it).
IndexDBName string
// BaselineDir / BaselineS3 are the resolved _snapshot baseline source, split
// by scheme (dir-preferred, mirroring the console's Time-travel tab). The
// #766 local→S3 fallback the console bundle also carries is intentionally
// dropped — a documented single-source-parity edge for the embedded port
// (see docs/time-travel-sql.md).
BaselineDir string
BaselineS3 string
// NoArchive disables archive auto-discovery for this server.
NoArchive bool
// DefaultSchema is the source database name (from the registry SourceDSN),
// seeded so USE-less `_flashback.<table>` queries resolve; empty for the
// boot entry (no registry SourceDSN).
DefaultSchema string
}
FlashbackTarget is the go-mysql-free resolution of a flashback connection's target server (issue #996): the per-source index handle, baseline source, and default schema the MySQL-protocol serving layer needs to build a shim handler.
It lives here — but the serving code does NOT — because internal/console is a read-layer package barred from linking go-mysql (#528, TestReadLayerDoesNotLinkGoMySQL). cmd/bintrail-console owns the protocol server and consumes this plain struct; nothing here imports the protocol or capture libraries.
type LeftOut ¶ added in v0.95.0
type LeftOut struct{ Table, Reason, Schema, Name string }
LeftOut is one table a full read left out of its snapshot and why: the converter's list, as this package takes it.
type LocationMigration ¶ added in v0.92.0
type LocationMigration struct {
// Done: the registry records the migration, from this start or an
// earlier one. False only before it runs.
Done bool `json:"-"`
// Migrated names the servers that got the process location at this start.
Migrated []string `json:"servers,omitempty"`
// MissingDir names the migrated servers whose folder does not exist right
// now. It is written all the same: it is what they read before, and a
// mount that is not there yet can come back.
MissingDir []string `json:"missing_dir,omitempty"`
// NotSaved is why the registry FILE does not hold the result, empty when
// it does or when nothing changed. This process reads the migrated values
// anyway; a later start migrates again until a save succeeds.
NotSaved string `json:"not_saved,omitempty"`
}
LocationMigration is what MigrateProcessBaselineLocation did at this start.
type MCPTokenFile ¶ added in v0.40.0
type MCPTokenFile struct {
Version int `yaml:"version"`
TokenSHA256 string `yaml:"token_sha256"`
CreatedAt string `yaml:"created_at"`
// Permissions is the grant set recorded at mint time (#1124): the minting
// session's permissions, so a token can never exceed what its minter could
// do through the browser API. A nil (absent) field means the FULL read
// surface — both the file a full-access minter writes (every OSS session,
// the static token, a password login) and every file minted before grants
// were recorded, which were all minted by full-access sessions in OSS
// builds. A present-but-empty list grants nothing; the pointer keeps
// "absent" and "empty" distinguishable across the YAML round trip.
Permissions *[]string `yaml:"permissions,omitempty"`
// Extra preserves unknown top-level fields a newer binary wrote, across
// this binary's load→save cycle.
Extra map[string]any `yaml:",inline"`
// contains filtered or unexported fields
}
MCPTokenFile is the on-disk managed MCP token: only the SHA-256 of the token is persisted (API-key pattern — the plaintext is shown once at generation and never stored), so a read of the file does not yield a usable credential. The versioned envelope and Extra leave room for additive fields without a format break (authfile.go precedent). Construct only via LoadMCPTokenFile / GenerateMCPToken — readOnly is set at load time.
func GenerateMCPToken ¶ added in v0.40.0
func GenerateMCPToken(path string, grants []ext.Permission) (string, *MCPTokenFile, error)
GenerateMCPToken mints a new managed token, persists its SHA-256 at path (atomic, 0600), and returns the plaintext and the stored record. A pre-existing file is replaced (rotate); a MALFORMED file is replaced too (the UI's Generate button is the documented self-heal); an unreadable file or a newer-version file refuses — what could not be inspected must not be destroyed.
grants is the permission set the token will carry on /mcp tool dispatch (#1124): nil records no permissions field — the full read surface, the full-access-minter case — while a non-nil slice (even empty) is recorded verbatim and caps the token at exactly those permissions. A rotate always re-records from the CURRENT minter, never from the replaced file.
func LoadMCPTokenFile ¶ added in v0.40.0
func LoadMCPTokenFile(path string) (*MCPTokenFile, error)
LoadMCPTokenFile reads the managed-token file at path. A missing file returns (nil, nil): no managed token configured. An unparseable file, or a malformed digest in a file THIS version owns, errors with errMCPTokenFileMalformed. A NEWER-version file is never an error for its payload: it loads read-only, and a digest this binary can't read simply never authenticates — a newer format must degrade, not brick startup or demand deletion (the readOnly contract).
func (*MCPTokenFile) ReadOnly ¶ added in v0.40.0
func (f *MCPTokenFile) ReadOnly() bool
ReadOnly reports whether write paths must refuse (newer-version file).
type MonitorController ¶
type MonitorController interface {
// DeriveIndexDSN returns the index DSN a monitored entry should use — a
// dedicated per-source database on the daemon's index MySQL server. It
// does not create anything; Start does.
DeriveIndexDSN(entryID string) (string, error)
// Doctor runs the preflight checks against the entry's source (and its
// index DSN, which may not exist yet — that is a pass, init creates it).
Doctor(ctx context.Context, e ServerEntry) (*DoctorReport, error)
// DoctorUnsaved runs the source half of those checks for a server that is
// not saved yet (#1767), the web interface's Test connection on a new
// server. It writes nothing anywhere: the index checks are left out (the
// write-access one creates a probe database), and the first schema
// snapshot counts as pending, since a new server's index does not exist.
DoctorUnsaved(ctx context.Context, e ServerEntry) (*DoctorReport, error)
// Start provisions the entry's index database (CREATE DATABASE + tables +
// schema migration — the supervisor is a WRITER, the same role the cmd
// layer plays for the boot DSN, so the console's never-migrates invariant
// holds) and launches the supervised stream. Idempotent for an already
// running entry. ctx bounds only the synchronous provisioning; the stream
// itself lives on the daemon's lifecycle.
Start(ctx context.Context, e ServerEntry) error
// Stop cancels the entry's stream and releases its advisory lock.
// Idempotent for an already stopped entry.
Stop(ctx context.Context, entryID string) error
// Status reports the entry's current monitor state.
Status(entryID string) MonitorStatus
}
MonitorController is the control-plane supervisor as the console sees it. All methods must be safe for concurrent use. Errors returned to handlers are written into HTTP responses — implementations must pre-scrub DSN secrets out of them.
type MonitorStatus ¶
type MonitorStatus struct {
// State: "stopped" | "pending" | "running" | "stalled" | "lost_position"
// | "failed".
// - "pending" covers launch through the stream's first checkpoint —
// connecting, snapshotting, discovering the start position. Only a
// saved checkpoint (or indexed batch) flips it to "running" (#407).
// - "stalled" and "lost_position" are running variants derived at read
// time: the stream goroutine is alive, but it has made no progress
// for several minutes (stalled) or an unfillable binlog gap forced
// it to skip permanently lost events (lost_position — durable,
// persisted in stream_state, re-hydrated on Start, cleared only by
// an explicit Stop).
// - "failed" carries LastError; the supervisor keeps retrying with
// backoff — "unhealthy, recovering", not terminal — unless the
// circuit breaker gave up after hours of continuous crash-looping
// (the message says so; Start re-arms it).
State string `json:"state"`
LastError string `json:"last_error,omitempty"`
// Since is when the underlying STORED state was entered (RFC3339), empty
// for stopped. For the derived stalled/lost_position presentations it
// still reflects the running transition, not when the stream stalled or
// lost its position.
Since string `json:"since,omitempty"`
// SourceConnected: the latest run's stream opened the source connection
// (#1606). It resets when a run starts, and keeps that run's value while
// the run waits to retry or after it stops.
SourceConnected bool `json:"source_connected,omitempty"`
// Retrying: a failed state the supervisor will retry on its own after a
// backoff. False for a failure it gave up on and for a Start that failed
// while setting up, which both wait for Start (#1606).
Retrying bool `json:"retrying,omitempty"`
// Phase names a long startup step the stream is inside right now, so
// "pending" can say WHICH part of starting up it is stuck on (#1690).
// "resume_cleanup": the pre-capture delete of events a replayed window
// would re-index, minutes of work on a large index.
// "resume_cleanup_waiting": that delete has not started, because one from
// an earlier run is still executing on the index server (#1708).
// Empty whenever no such step is running — including between retries and
// for a stream that never reached one.
Phase string `json:"phase,omitempty"`
// PhaseDetail qualifies Phase in a few words, for display only. For
// "resume_cleanup_waiting" (#1708) it names the earlier cleanup being
// waited on: "connection 812, running for 14m0s". Empty with Phase.
PhaseDetail string `json:"phase_detail,omitempty"`
// ErrorCode names the cause of a "failed" state for the causes a screen
// acts on (MonitorErr*). Empty for every other failure, which LastError
// describes. A screen decides on this and never on LastError's text.
ErrorCode string `json:"error_code,omitempty"`
// FlavorWarning, when set, says the server contradicts the Source type
// saved with its entry. Capture follows the server; this is a warning to
// show, never a failure.
FlavorWarning string `json:"flavor_warning,omitempty"`
}
MonitorStatus is the supervisor's view of one entry's stream.
type NewEntryDiscarder ¶ added in v0.87.0
type NewEntryDiscarder interface {
DiscardNew(ctx context.Context, e ServerEntry) error
}
NewEntryDiscarder is implemented by a supervisor that can take back what a failed first Start provisioned for an entry created in the same request: its job slot and the per-server index database Start may already have created. Optional, and checked with a type assertion, so MonitorController does not grow a method every implementation must carry; only the Connect check's rollback calls it (#1803).
type ParsedBackupSchedule ¶ added in v0.71.0
type ParsedBackupSchedule struct {
Every time.Duration
At time.Duration // offset from midnight UTC
// FullEvery is the full-copy timetable (#1564); 0 for none.
FullEvery time.Duration
}
ParsedBackupSchedule is a validated schedule, ready for slot arithmetic.
func (ParsedBackupSchedule) BackupsPer30Days ¶ added in v0.71.0
func (p ParsedBackupSchedule) BackupsPer30Days() int64
BackupsPer30Days is how many backups this schedule publishes in 30 days: every one a full-table snapshot, and on a server without an S3 destination none of them ever removed automatically. A full-backup timetable (#1564) adds the full backups that do not land on a run (the two grids share their anchor, so they coincide every lcm(Every, FullEvery)).
func (ParsedBackupSchedule) FullCopiesPer30Days ¶ added in v0.86.0
func (p ParsedBackupSchedule) FullCopiesPer30Days() int64
FullCopiesPer30Days is how many full copies the full-copy timetable asks for in 30 days: each one a full read of the source database.
func (ParsedBackupSchedule) FullSlotAtOrBefore ¶ added in v0.86.0
func (p ParsedBackupSchedule) FullSlotAtOrBefore(now time.Time) time.Time
FullSlotAtOrBefore and NextFullRun are SlotAtOrBefore and NextRun on the full-copy timetable (#1564): the same fixed grid, anchored on the same At, stepped by FullEvery. Zero without one. Because both grids share the anchor, a FullEvery that is a whole multiple of Every lands every full copy ON a scheduled run, and that run is the full copy; any other FullEvery puts some full copies between runs, as runs of their own.
func (ParsedBackupSchedule) NextFullRun ¶ added in v0.86.0
func (p ParsedBackupSchedule) NextFullRun(now time.Time) time.Time
func (ParsedBackupSchedule) NextRun ¶ added in v0.71.0
func (p ParsedBackupSchedule) NextRun(now time.Time) time.Time
NextRun returns the first slot strictly after now.
func (ParsedBackupSchedule) SlotAtOrBefore ¶ added in v0.71.0
func (p ParsedBackupSchedule) SlotAtOrBefore(now time.Time) time.Time
SlotAtOrBefore returns the latest slot at or before now.
type RefusedTable ¶ added in v0.91.0
type RefusedTable struct {
Name string `json:"name"`
Verdict string `json:"verdict"`
Reason string `json:"reason,omitempty"`
}
RefusedTable is one table that stopped a refresh or a restore, with the verdict the command line prints for it (reconstruct.RefreshOutcomes) and the reason. Publication is all-or-nothing, so each of these stopped the copy of every table, not only its own.
Verdict is kept as written: a file from a newer version may carry one this version does not know, and the page shows it as it is.
func LeftOutTablesOf ¶ added in v0.95.0
func LeftOutTablesOf(left []LeftOut, secrets ...string) (kept []RefusedTable, omitted int)
LeftOutTablesOf keeps the tables a full read left out, as RefusedTables, with the same bounds and scrubbing as RefusedTablesOf.
func RefusedTablesOf ¶ added in v0.91.0
func RefusedTablesOf(outcomes []reconstruct.RefreshOutcome, secrets ...string) (kept []RefusedTable, omitted int)
RefusedTablesOf keeps the refused tables of a run, in the order the run listed them, up to RefusedTablesCap, and counts the ones left out. Tables that were refreshed, unchanged or skipped are not kept: they did not stop anything.
secrets are the connection strings the run used. Each one, and the password inside it, is removed from every reason, and so is the credentials part of any URL or DSN: the reason is an error text written far from here, and it reaches a browser and a file on disk.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry is the console's named-server store: a local YAML file, the ONLY thing the console ever writes. All mutations rewrite the file atomically (temp file + fsync + rename) under a mutex — this is bintrail's first programmatically-mutated config file, so a plain os.WriteFile (which can interleave concurrent writers) is not enough.
func LoadRegistry ¶
LoadRegistry reads the registry file at path. A missing or empty file is an empty registry, not an error. The parse is deliberately NON-strict (unlike shim.yaml's UnmarshalStrict): an older binary must tolerate top-level fields a newer one added. path == "" creates an in-memory registry that never touches disk — for unit tests and callers without persistence.
func (*Registry) Add ¶
func (r *Registry) Add(e ServerEntry) (ServerEntry, error)
Add validates, mints an id, appends, and persists the entry. The returned entry carries the generated ID.
func (*Registry) AddAutoNamed ¶ added in v0.87.0
func (r *Registry) AddAutoNamed(e ServerEntry, base string) (ServerEntry, error)
AddAutoNamed is Add for an entry nobody named: when e.Name is empty and base is not, the name is derived from base under the SAME lock the append takes, so two operators adding the first server for one host cannot both derive the same name and have one of them refused as a duplicate.
A name the caller DID supply is used verbatim, duplicate refusal included — deriving is what happens when nobody chose, never a silent rename of what somebody did choose. e.Name is compared to "" rather than trimmed, because that is exactly the emptiness rule checkName applies one line later; the handlers trim before they get here.
func (*Registry) BackupSettings ¶ added in v0.85.0
func (r *Registry) BackupSettings() BackupSettings
BackupSettings returns the saved daemon-wide backup settings. Always a value: an absent section is an empty one, and every accessor answers "nothing saved" for every key.
func (*Registry) CorrectSourceFlavor ¶ added in v0.92.0
CorrectSourceFlavor saves detected, the flavor the server reported when capture asked it, as the Source type of entry id, and reports whether the saved value changed. For MySQL and MariaDB the saved type is a hint that capture already overrides; this makes the label every other reader sees (the server list, the snapshot trigger, the capture status read) say what capture runs as. Only a move between mysql and mariadb is made: an entry saved as postgres is never touched, and detected must be mysql or mariadb. A saved value that already means detected is left as written, so an old blank entry of a MySQL server stays blank.
func (*Registry) DefaultBaselineDir ¶ added in v0.87.0
DefaultBaselineDir is where server id keeps its local snapshots unless the operator names another folder: <state dir>/snapshots/<id>, where the state directory is the one holding this registry file. Keyed by the id (random hex, stable, path-safe), never by the display name, which is editable free text: renaming a server must not orphan its snapshots. "" for an in-memory registry, which has no state directory.
func (*Registry) Get ¶
func (r *Registry) Get(id string) (ServerEntry, bool)
Get returns the entry with the given id. Like List, the entry's Extra map is shared with the registry — treat it as read-only.
func (*Registry) List ¶
func (r *Registry) List() []ServerEntry
List returns a copy of the entries, in file order. The copy is shallow: each entry's Extra map is shared with the registry — treat returned entries as read-only (nothing in the console mutates Extra off a copy today).
func (*Registry) LocationMigration ¶ added in v0.92.0
func (r *Registry) LocationMigration() LocationMigration
LocationMigration returns what the migration did at this start, for the Snapshots page.
func (*Registry) MigrateProcessBaselineLocation ¶ added in v0.92.0
func (r *Registry) MigrateProcessBaselineLocation(dir, s3 string) LocationMigration
MigrateProcessBaselineLocation writes the process-wide snapshot location into every registry server that has none of its own, exactly as the old read-path fallback resolved it: all or nothing (a server naming its own folder OR bucket keeps just that), both values copied verbatim, untrimmed, empty ones included. It runs once per registry: the envelope records it, and a registry that records it is left alone. A start with no process location records nothing: there is nothing to migrate, and recording it would let a start that merely lacked the flag (a serve run without --baseline-dir, a missing variable) cancel the migration of a later start that has it, for good.
The entries change in memory first and the file is saved once. When the file cannot be saved (written by a newer version, or the write failed), the in-memory entries keep the migrated values, so this process reads what it read yesterday, and the report says the file was not updated.
The command-line server is not in the registry and is never touched.
func (*Registry) NameFor ¶ added in v0.87.0
NameFor is the name an add would give an entry right now: typed when it is not empty, else the automatic name made unique against the registry, worked out under the same lock AddAutoNamed takes. It reserves nothing — another add may take the name first — so it describes, and AddAutoNamed decides.
func (*Registry) Path ¶ added in v0.87.0
Path returns the registry file's path, or "" for an in-memory registry. Console state files that belong beside it are named from this.
Nil-safe: Config.Registry is optional, so a nil *Registry is a real shape in this package, and "no registry" and "an in-memory registry" mean the same thing to every caller of this — nothing on disk to sit beside.
func (*Registry) ReadOnly ¶
ReadOnly reports whether mutating operations are refused (newer-version file).
func (*Registry) Rotation ¶ added in v0.13.0
func (r *Registry) Rotation() (RotationConfig, bool)
Rotation returns the saved global rotation policy, or false when none is set (the daemon's --rotate-* flags/env are in force). The rotation loop's settings provider reads this every cycle, so an override applies live.
func (*Registry) SetBackupSetting ¶ added in v0.85.0
SetBackupSetting saves one key, or clears it when value is nil — clearing is what hands the setting back to the flag or environment variable, and without it the interface would be a one-way door. The in-memory value rolls back if the write fails, so the file and this process never disagree.
The value is stored verbatim, trimmed. Grammar is the caller's to validate: the endpoint refuses what the daemon cannot parse BEFORE it gets here, so a value that reaches the file is one some consumer can read.
func (*Registry) SetCommandLineWriter ¶ added in v0.92.0
SetCommandLineWriter records the command-line server's location for a process that writes snapshots for it, so WriteRefusal can count it. Called once where the registry is loaded.
func (*Registry) SetProcessS3Location ¶ added in v0.82.0
SetProcessS3Location registers an S3 location the daemon itself reads: the --baseline-s3 of the boot entry, which the servers that relied on it before #1684 were given as their own (MigrateProcessBaselineLocation), so they still read it the way they always did. Its bucket counts as a bucket with no store: a server naming it with a store is refused, and a hand-edited one leaves it unrouted. label names the setting in refusals and warnings. Called before serving.
func (*Registry) SetRotation ¶ added in v0.13.0
func (r *Registry) SetRotation(rc RotationConfig) error
SetRotation persists the global rotation policy. Like every registry mutation it rewrites the file atomically and is refused on a newer-version (read-only) file. The caller validates the field grammar.
func (*Registry) UndoAdd ¶ added in v0.87.0
UndoAdd removes an entry Add just created, when the rest of the create failed. Unlike Delete it marks no folder held (#1681): the entry never took a snapshot, so the folder it pointed at holds nothing of its.
func (*Registry) Update ¶
func (r *Registry) Update(e ServerEntry) error
Update replaces the entry with e.ID and persists. The caller is responsible for keep-password merging — the registry stores exactly what it is given. Unknown phase-2 fields survive: the stored entry's Extra is carried over unless the caller supplied its own.
func (*Registry) WriteRefusal ¶ added in v0.92.0
func (r *Registry) WriteRefusal(e ServerEntry) error
WriteRefusal is SharedLocationRefusal over this registry: the refusal for a write by the server with e's id, or nil. Nil-safe, and nil for an id the registry does not hold (the command-line server).
type RotationConfig ¶ added in v0.13.0
type RotationConfig struct {
Retain string `yaml:"retain"`
Interval string `yaml:"interval"`
AddFuture int `yaml:"add_future"`
}
RotationConfig is the daemon-global built-in-rotation policy, editable from the console UI. It is stored once in the registry envelope, NOT per-server: the rotation loop is a single shared ticker, so retain/interval/add-future necessarily apply to every index the daemon rotates. Absent (nil) = the daemon's --rotate-* flags / BINTRAIL_ROTATE_* env stay in force. The fields hold the operator-typed strings (e.g. "30d", "1h") so they round-trip exactly and the engine parses them with the same grammar as the flags.
type RotationDefaults ¶ added in v0.13.0
type RotationDefaults struct {
Retain string
Interval string
AddFuture int
// Enabled is false when the daemon was started with rotation off
// (--rotate-retain off). The loop is not running then, so a console-saved
// override would need a restart — the panel warns in that state.
Enabled bool
}
RotationDefaults is the daemon-side built-in-rotation policy, surfaced to the console read-only as the fallback shown when no override is saved.
type SQLExportRequest ¶ added in v0.66.0
type SQLExportRequest struct {
ServerID string
ServerName string
IndexDSN string
BaselineSrc string // local directory or s3:// prefix
At time.Time
}
SQLExportRequest identifies the server and the instant to build for.
type SQLExportStagedBuild ¶ added in v0.71.0
type SQLExportStagedBuild struct {
ServerID string
State string // running | succeeded | failed (only while its removal is still owed) | replaced (a previous build a newer one could not remove)
At string // the instant the dump represents (RFC3339 UTC)
ExpiresAt string // when a finished build is removed unless downloaded first
// Bytes is the live on-disk size; meaningful only when BytesKnown. A
// walk that failed part-way reports unknown, never the fraction it
// managed to count.
Bytes int64
BytesKnown bool
// StagingError is why the build is still on disk when it should not be
// (a removal that failed) or why it cannot be read; empty when fine.
StagingError string
}
SQLExportStagedBuild is one build on disk: in progress, finished and waiting for its download, or owed a removal that has not succeeded yet.
type SQLExportStagingInfo ¶ added in v0.71.0
type SQLExportStagingInfo struct {
Dir string // the staging base every build lives under
TTL time.Duration // how long a finished build stays downloadable
Builds []SQLExportStagedBuild // sorted by server id
}
SQLExportStagingInfo is what the sql-export staging holds right now.
type SQLExporter ¶ added in v0.66.0
type SQLExporter interface {
// TriggerSQLExport starts the build asynchronously. ErrBaselineRunning
// when another baseline job for the server is in flight.
TriggerSQLExport(req SQLExportRequest) error
// SQLExportStatus reports the last build for a server (idle if none).
SQLExportStatus(serverID string) BaselineStatus
// SQLExportDir returns the finished dump's directory and the status it
// belongs to, from one locked snapshot (so the caller can never label
// one build's bytes with another build's instant); ok is false until a
// build has succeeded and its directory affirmatively carries the
// completeness marker.
SQLExportDir(serverID string) (dir string, status BaselineStatus, ok bool)
// SQLExportHold registers a download of the build in dir so the
// exporter neither expires nor removes it under the stream; release
// drops the hold (idempotent). ok is false when dir is no longer the
// server's finished build, in which case the caller answers not-ready.
SQLExportHold(serverID, dir string) (release func(), ok bool)
// SQLExportDelivered tells the exporter the whole archive built in dir
// was written to the client, so the staged copy can go now rather than
// at its download deadline. dir pins which build: a newer build that
// replaced it mid-stream must not be removed on the strength of the old
// one's download.
SQLExportDelivered(serverID, dir string)
// SQLExportStaged reports every build currently on disk (running or
// waiting for its download) with its live size, for the Storage page.
SQLExportStaged() SQLExportStagingInfo
}
SQLExporter builds a custom .sql backup: fold the backup at-or-before an operator-chosen instant forward through the index and write it as a mydumper-format dump the operator downloads and loads with myloader (or the mysql client, applying mydumper's session assumptions), no bintrail needed on the restore side. A separate interface from the other baseline controllers so its wiring CAN diverge (the #1171 lesson); today it deliberately rides the same supervisor as BaselineRestore — see wireBaselineExtras for why that derivation is safe here.
type ScheduleRefusal ¶ added in v0.71.0
type ScheduleRefusal struct{ Reason string }
ScheduleRefusal is CheckBackupSchedule's error: the class plus the reason.
func (*ScheduleRefusal) Error ¶ added in v0.71.0
func (r *ScheduleRefusal) Error() string
func (*ScheduleRefusal) Is ¶ added in v0.71.0
func (r *ScheduleRefusal) Is(target error) bool
Is makes errors.Is(err, ErrBackupScheduleNotRunnable) true.
type SchemaSnapshotController ¶ added in v0.51.0
type SchemaSnapshotController interface {
// Trigger takes a fresh snapshot in the background and returns immediately.
// Returns ErrSchemaSnapshotRunning if one is already running for
// req.ServerID.
Trigger(req SchemaSnapshotRequest) error
// Status reports the latest known state for a server (idle if none has run
// in this process).
Status(serverID string) SchemaSnapshotStatus
}
SchemaSnapshotController re-reads a monitored source's column layout into the index and puts the running capture stream onto it (#1296).
A SCHEMA SNAPSHOT IS NOT A BASELINE, and this file is where the two are kept apart. A schema snapshot is the record of each table's columns that capture decodes row events against — cheap, metadata-only, read from information_schema. A baseline (BaselineController) is a full COPY of the data, produced by mydumper. The console offers both, and the capture-degraded banner used to say "take a fresh snapshot" next to a button labelled "Create baseline": an operator who follows that runs a dump they did not need while capture stays broken. Keep the two vocabularies separate in every type name, endpoint and label here.
Wired in only by `bintrail-console watch`, which owns the supervised streams; nil on the standalone read-only console, where the endpoints refuse with 403 (mirroring how MonitorController gates the monitor verbs).
type SchemaSnapshotRequest ¶ added in v0.51.0
type SchemaSnapshotRequest struct {
ServerID string
ServerName string
SourceDSN string
// IndexDSN is the entry's per-source index database — where the new
// snapshot rows are written and where the restarted stream will read them
// from. The two must be the same database or the stream would reload the
// snapshot it already had.
IndexDSN string
Schemas []string
// SourceSSL is how the source connection uses TLS: the entry's
// SourceSSL, so the snapshot reads the source the way capture does.
SourceSSL config.SSL
}
SchemaSnapshotRequest is the in-process job description the endpoint hands the controller. Both DSNs are secrets: they stay inside the process and are never written to disk or serialized into any HTTP response.
type SchemaSnapshotStatus ¶ added in v0.51.0
type SchemaSnapshotStatus struct {
State string `json:"state"` // idle | running | succeeded | failed
Since string `json:"since,omitempty"`
FinishedAt string `json:"finished_at,omitempty"`
LastError string `json:"last_error,omitempty"`
SnapshotID int `json:"snapshot_id,omitempty"`
Tables int `json:"tables,omitempty"`
// ExcludedTables names tables snapshot validation left out (no explicit
// primary key / non-InnoDB). Reported because those tables are exactly the
// ones that will KEEP being skipped after this run — an operator who
// pressed the button to fix capture must not read "succeeded" as "every
// table is captured now".
ExcludedTables []string `json:"excluded_tables,omitempty"`
// StreamReloaded reports whether the capture stream was actually restarted
// onto the new snapshot. A running stream holds its resolver in memory and
// only swaps it on a DDL event, so a snapshot written underneath it changes
// NOTHING until the stream reloads. Without this field a silent no-op would
// be indistinguishable from a fix — the precise failure this feature exists
// to prevent.
StreamReloaded bool `json:"stream_reloaded"`
// ReloadError explains a failed reload. The snapshot itself is still
// durable, so this is not a job failure — it is "the new snapshot is
// recorded but capture is still running on the old one; restart it".
ReloadError string `json:"reload_error,omitempty"`
}
SchemaSnapshotStatus is the pollable state of a server's most recent snapshot job.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server is a configured, ready-to-run console HTTP server. It holds only process-global state; everything per-server (db, engine, resolver, baseline gates) lives in a connManager bundle resolved per request from the X-Bintrail-Server header.
func New ¶
New validates the config, seeds the boot connection bundle, and assembles the middleware/route tree. It does no network I/O — call Run to listen.
func (*Server) Listen ¶
Listen binds the configured address and returns the listener synchronously, so a caller can fail fast on a port conflict before reporting the server as ready (the standalone command blocks on Run, but `bintrail-console watch` starts the server in a goroutine and must surface a bind error up front).
func (*Server) NeedsSetup ¶ added in v0.12.0
NeedsSetup reports whether the console is in first-run setup: no credential at all, on a loopback bind (or a non-loopback bind the operator marked access-controlled via --allow-setup), so the operator must create a password in the browser (via the unauthenticated /api/auth/setup endpoint). The cmd layer uses it to print "create your password" instead of a URL with a token.
func (*Server) PasswordLogin ¶ added in v0.11.0
PasswordLogin reports whether a console password was configured at boot — the cmd layer keys its banner wording on it.
func (*Server) ResolveFlashback ¶ added in v0.31.0
ResolveFlashback maps a flashback connection username to its target server's per-source index + baseline, opening the connection lazily via connManager. The username selects the server by registry ID, display Name, or "default" (the boot entry). Returns ErrUnknownServer when the selector matches no selectable server — the serving layer turns that into a MySQL "no such database" error on the client's first query. The registry is read live, so servers added in the console mid-session are reachable without a restart.
func (*Server) Run ¶
Run binds and serves, blocking until ctx is cancelled, then shuts down gracefully. Returns the bind/serve error, or nil on a clean shutdown.
func (*Server) Serve ¶
Serve serves on ln and blocks until ctx is cancelled, then shuts the server down gracefully (5s drain). It takes ownership of ln (Shutdown closes it). Lazily-opened registry connections are closed on the way out; the boot entry's db belongs to the cmd layer and its deferred Close.
func (*Server) Token ¶
Token returns the static automation token — never the UI-managed MCP token (the flashback port depends on that distinction). Empty in password-only mode.
type ServerEntry ¶
type ServerEntry struct {
// ID is a server-generated stable 8-byte hex key; it survives renames and
// is what the browser sends in the X-Bintrail-Server header.
ID string `yaml:"id"`
// Name is the mutable operator-facing label, unique across the registry.
Name string `yaml:"name"`
// DSN is the full index DSN, password included — a secret at rest (the
// file is 0600, same class as shim.yaml's mysql_password).
DSN string `yaml:"index_dsn"`
BaselineDir string `yaml:"baseline_dir,omitempty"`
BaselineS3 string `yaml:"baseline_s3,omitempty"`
NoArchive bool `yaml:"no_archive,omitempty"`
// SourceDSN is the source MySQL to monitor — replication credentials,
// a secret exactly like DSN, never serialized to any HTTP response.
// Empty = a view-only entry (no monitoring configured).
SourceDSN string `yaml:"source_dsn,omitempty"`
// SourceServerID overrides the auto-derived replica server id (0 = derive
// from the source DSN, the same rule as `bintrail up`).
SourceServerID uint32 `yaml:"source_server_id,omitempty"`
// Schemas is the optional comma-separated schema filter for monitoring.
Schemas string `yaml:"schemas,omitempty"`
// ── Source family (#1019) ──
// Flavor selects the capture engine: "" or "mysql" (MySQL), "mariadb"
// (MySQL DSN, MariaDB GTID at stream), "postgres" (pgstreamrun). Empty →
// mysql keeps every pre-#1019 entry working (mirrors an empty SSLMode).
// This is the generic "which source family" field #623 (MariaDB) also
// needs — one implementation, not two. Accessed via SourceFlavor().
Flavor string `yaml:"flavor,omitempty"`
// SourceSlot / SourcePublication configure PostgreSQL logical replication
// (postgres flavor only): the operator-created publication and the
// replication slot the capturer streams from. Empty for MySQL/MariaDB.
SourceSlot string `yaml:"source_slot,omitempty"`
SourcePublication string `yaml:"source_publication,omitempty"`
// ── Source TLS (#879) ──
// SSLMode/SSLCA/SSLCert/SSLKey configure the TLS the supervisor uses when
// connecting to the SOURCE MySQL. Empty SSLMode = the daemon default
// ("preferred"), preserving pre-#879 behavior; SSLCA/SSLCert/SSLKey are file
// paths on the daemon host (verify-ca / mutual TLS), not secrets like the
// DSNs. On binaries that predate these they round-trip untouched via Extra.
SSLMode string `yaml:"ssl_mode,omitempty"`
SSLCA string `yaml:"ssl_ca,omitempty"`
SSLCert string `yaml:"ssl_cert,omitempty"`
SSLKey string `yaml:"ssl_key,omitempty"`
// ArchiveS3 is the S3 destination (s3://bucket/prefix/) the daemon's
// built-in rotation uploads this source's rotated Parquet partitions to
// BEFORE dropping them — so the forensic record survives retention and
// stays queryable (the console auto-discovers it). Empty = drop-only.
// Region/credentials come from the ambient AWS chain (env / ~/.aws / IAM
// role); a local staging dir is used transiently. Non-secret (a bucket
// URL): unlike DSNs it is serialized to the masked HTTP responses.
ArchiveS3 string `yaml:"archive_s3,omitempty"`
// ── S3 store (#1575) ──
// S3Endpoint / S3PathStyle / S3Region say where this server's S3 buckets
// (ArchiveS3 and BaselineS3) live when that is not AWS or the process-wide
// BINTRAIL_S3_ENDPOINT: an S3-compatible store (MinIO, Wasabi), a pinned
// region, or both. Applied PER BUCKET at runtime (storage.BucketStore):
// every upload to and every DuckDB read of a bucket named here goes to
// this store, whichever server asked. Two servers naming the same bucket
// with different stores is refused (ErrS3StoreConflict). S3PathStyle is
// "", "path" or "vhost"; "" means path style, what MinIO needs. Locations,
// not secrets: serialized to the masked DTO.
S3Endpoint string `yaml:"s3_endpoint,omitempty"`
S3PathStyle string `yaml:"s3_path_style,omitempty"`
S3Region string `yaml:"s3_region,omitempty"`
// S3AccessKeyID / S3SecretAccessKey sign requests for those buckets
// instead of the ambient credential chain. Both or neither. They belong
// to the bucket like the endpoint: two servers on one bucket must carry
// the same pair. The secret is at rest here like the DSN passwords (file
// 0600) and never leaves the process: the DTO carries the access key and
// has_s3_secret_access_key only.
S3AccessKeyID string `yaml:"s3_access_key_id,omitempty"`
S3SecretAccessKey string `yaml:"s3_secret_access_key,omitempty"`
// MonitorDesired records the operator's intent to monitor this source.
// The supervisor reconciles running streams against it at boot and on
// every edit; nothing reads it until phase 3.
MonitorDesired bool `yaml:"monitor_desired,omitempty"`
// BackupSchedule is this server's unattended backup timer (#1442): a full
// backup from the source, or a rebuild from the change history, on a
// fixed grid. Nil = no schedule. Non-secret. Edited only through the
// schedule endpoints; the server edit form carries it over untouched.
// On binaries that predate it, it round-trips through Extra.
BackupSchedule *BackupSchedule `yaml:"backup_schedule,omitempty"`
// LocalKeepNewest is this server's local snapshot retention (#1681):
// while it has a local folder (BaselineDir) and no external destination
// (BaselineS3), the daemon keeps the newest LocalKeepNewest complete
// snapshots there and removes older ones. 0 (every entry saved before
// #1681) keeps them all, as before. With a destination set it does
// nothing: that mode removes only what the destination confirmed.
LocalKeepNewest int `yaml:"local_keep_newest,omitempty"`
// LocalKeepHeldDir names a folder that stopped being shared while it
// still holds snapshots another server wrote (#1681): the other server
// was deleted, answered no, or moved away. A snapshot does not record
// which server wrote it, so counting the folder would take the other
// server's snapshots for this one's copies; while BaselineDir is this
// folder (heldNow) it is never pruned, like a shared folder. A FOLDER,
// not a yes/no, so answering no and then yes, or moving away and back,
// finds it held again; a new empty folder is not it (the way out the
// page names). OWNED BY THE REGISTRY: written by markHeldFolders only,
// carried across every update, never taken from a caller.
LocalKeepHeldDir string `yaml:"local_keep_held_dir,omitempty"`
// Extra is the forward-compat catch-all: unknown fields written by a NEWER
// bintrail (e.g. the phase-2 control plane's source_dsn / server_id /
// monitor_state) land here on load and re-emit verbatim on save, so an
// older binary editing the file never drops them. yaml:",inline" requires
// map[string]any specifically; non-strict Unmarshal alone would only
// tolerate unknown fields on read — a re-marshal would lose them.
Extra map[string]any `yaml:",inline"`
}
ServerEntry is one named index connection in the registry. DSN holds the full secret (including the password) and is NEVER serialized into an HTTP response — see serverDTO in servers_api.go for the masked wire view.
func (ServerEntry) BucketStore ¶ added in v0.82.0
func (e ServerEntry) BucketStore() (storage.BucketStore, error)
BucketStore builds the per-bucket store this entry's S3 settings describe. The zero store (no endpoint, no region, no keys) means the ambient configuration.
func (ServerEntry) IsPostgres ¶ added in v0.33.0
func (e ServerEntry) IsPostgres() bool
IsPostgres reports whether the entry's source is PostgreSQL.
func (ServerEntry) SourceFlavor ¶ added in v0.33.0
func (e ServerEntry) SourceFlavor() string
SourceFlavor returns the entry's normalized source family, defaulting a blank (pre-#1019) entry to mysql. Consumed by the monitor supervisor (#1020), the baseline trigger (#1021), and the console UI gating. A stored value that no longer normalizes (only reachable via a hand-edit — create/update validate on write) degrades to mysql, which fails loudly at doctor time rather than silently corrupting anything.
func (ServerEntry) SourceSSL ¶ added in v0.94.0
func (e ServerEntry) SourceSSL() config.SSL
SourceSSL is the TLS the SOURCE connection uses: the entry's ssl_* fields, with an empty ssl_mode meaning DefaultSourceSSLMode. Capture and every check the console runs against the source read it here, so a check never connects differently from the capture it is checking (a server that only accepts encrypted connections failed the startup checks in cleartext while capture, over TLS, would have worked). The mode is passed through as written: a misspelled one fails in config.BuildTLSConfig, for both.
type SnapshotFailure ¶ added in v0.93.0
type SnapshotFailure struct {
Kind string `json:"kind,omitempty"`
// Grant is the statement to run on the source, for missing_permission.
Grant string `json:"grant,omitempty"`
// Privileges is how many privileges Grant gives, so the page says "one
// more permission" only when it is one.
Privileges int `json:"privileges,omitempty"`
// MinVersion is the mydumper version to install, for mydumper_too_old.
MinVersion string `json:"min_version,omitempty"`
// Summary is the error's first line without mydumper's output, capped,
// for a failure with no kind: the one hint the card shows on screen.
Summary string `json:"summary,omitempty"`
// Server is the server's name, so a notice says which one failed.
Server string `json:"server,omitempty"`
// Scheduled: the backup schedule started this run, not a person.
Scheduled bool `json:"scheduled,omitempty"`
// Postgres: a Postgres source. Its snapshot creates a replication slot
// on the source that a failure can leave behind, so the card does not
// say the database was not changed.
Postgres bool `json:"postgres,omitempty"`
// ModeChosen: the lock mode this run tried last was an operator's
// choice (a startup variable or a saved setting), not the automatic one,
// so the fix may be to undo that choice.
ModeChosen bool `json:"mode_chosen,omitempty"`
}
SnapshotFailure says, as data, why a full read of a MySQL or MariaDB server failed when that is one of the few causes the page can explain in plain words (#1986). The error string beside it (LastError, Error) stays as it was, for the page's "Technical details" fold and for API clients.
Every failed run that published nothing carries one; a failure with no Kind gets the generic card, with Summary as its hint. Kind is set only where the cause is certain, because a wrong one (a GRANT for the wrong account or privilege) sends the operator to fix the wrong thing, which is worse than none.
type TelemetryController ¶ added in v0.42.0
type TelemetryController interface {
Enabled() bool
Decision() telemetry.Decision
SetRuntimeConsent(enabled bool)
// RecordDaemonCommand starts a span for one console action. The daemon
// variant is mandatory here: the console shares the daemon's months-lived
// run_id, so a plain command span would turn per-action events into a
// per-install activity timeline. Returns nil when telemetry is off.
RecordDaemonCommand(command string) *telemetry.Span
}
TelemetryController is the live usage-telemetry surface a long-running console wires in, so the UI opt-out toggle takes effect on the running process immediately instead of only on the next start. `watch` wires *telemetry.Client directly; the read-only `serve` wires a consent-only adapter whose RecordDaemonCommand returns nil, because it beacons (#1362) but must not record console actions. nil means no live process to control — the toggle still persists the machine-wide choice to the consent file.
type VerifyCellDiff ¶ added in v0.26.0
type VerifyCellDiff struct {
Column string `json:"column"`
Recovery string `json:"recovery"`
Baseline string `json:"baseline"`
}
VerifyCellDiff is the wire view of verify.CellDiff.
type VerifyController ¶ added in v0.26.0
type VerifyController interface {
// Trigger starts a verify run in the background and returns immediately.
// Returns ErrVerifyRunning if one is already running for req.ServerID.
Trigger(req VerifyRequest) error
// Status reports the latest known run for a server (idle if never run in
// this process): overall state plus per-table results accumulated so far.
Status(serverID string) VerifyStatus
// Explain drills into one table the last completed baseline-anchored run
// reported as a mismatch, re-running the row-level diff on demand — never
// precomputed for every mismatch (internal/verify.ExplainBaselinePairMismatch
// re-reconstructs the table). Returns ErrExplainUnavailable when no such
// run/table exists (including: the last run was live-source, which has no
// explain support in the engine).
//
// It MUST NOT block on the reconstruction (#1375): implementations start
// the work in the background and return ErrExplainRunning immediately,
// answering with the result on a later call. The reconstruction takes
// minutes on a large table, and a synchronous answer cannot outlive a
// fronting proxy's read timeout — so a blocking implementation makes the
// feature unusable on exactly the tables an operator most needs it for.
Explain(serverID, schema, table string) (*VerifyExplanation, error)
}
VerifyController runs bintrail verify's engine in-process for a monitored server (#677) — the same internal/verify functions internal/cli/verify.go calls, looped over a server's tables in a background goroutine so the console can poll per-table results as they land. It is wired in ONLY by `bintrail-console watch` when the operator opts in (BINTRAIL_CONSOLE_VERIFY_TRIGGER=1) or schedules verification (--verify-interval, #1191 — scheduling verify implies wanting verify); nil on the standalone read-only console, where the endpoints refuse with 403 — mirroring how BaselineController gates in-process baseline creation.
type VerifyExplanation ¶ added in v0.26.0
type VerifyExplanation struct {
Schema string `json:"schema"`
Table string `json:"table"`
Anchor string `json:"anchor"`
Total int `json:"total"`
Diffs []VerifyRowDiff `json:"diffs"`
Rendered string `json:"rendered"`
}
VerifyExplanation is the wire view of verify.MismatchExplanation: the structured per-row diffs (Diffs/Total, capped and overflow-summarized exactly like the CLI) plus Rendered — the same text internal/verify's MismatchExplanation.Write produces (including the deferred-type caveat and overflow breakdown, which live in unexported fields and so are only available through the rendered text, not the structured Diffs).
type VerifyHistory ¶ added in v0.49.0
type VerifyHistory struct {
// contains filtered or unexported fields
}
VerifyHistory is the persisted verify-run history (#1191): one JSON file, capped per server, written atomically (temp file + fsync + rename, 0600) like the server registry. It is console-local state on disk — alongside the registry, the auth file and the managed MCP token — not a new class of console write.
It deliberately lives in a console-local file rather than a table in the index database: scheduled verify covers registry servers, and registry DSNs never receive EnsureSchema/DDL (an existing invariant this must not bend).
func OpenVerifyHistory ¶ added in v0.49.0
func OpenVerifyHistory(path string) (*VerifyHistory, error)
OpenVerifyHistory loads the history at path. A missing file is an empty history. A corrupt or newer-versioned file is reported as an error — the caller decides whether to run without history rather than silently truncating a file a newer binary may still want.
func (*VerifyHistory) Append ¶ added in v0.49.0
func (h *VerifyHistory) Append(rec VerifyRunRecord) error
Append records one run and saves the file. The per-server cap is enforced here (oldest dropped). Called from the supervisor's finish path and the scheduler's skip path; a save failure is returned for the caller to log — history is an observability aid and must never fail a verify run.
func (*VerifyHistory) Found ¶ added in v0.49.0
func (h *VerifyHistory) Found() bool
Found reports whether a history file existed when this history was opened. A consumer that reports on verification activity MUST keep this distinct from an opened-but-empty history: the file is written only by `bintrail-console watch`, so a CLI-only deployment has no history at all, and rendering that absence like "no failed runs" would state a clean verification record for a period nothing ever verified.
func (*VerifyHistory) List ¶ added in v0.49.0
func (h *VerifyHistory) List(serverID string) []VerifyRunRecord
List returns the recorded runs for a server, newest first, each with its verdict filled (VerifyStatus.WithVerdict; never stored, so records written before the field existed get one too). The returned slice is a copy — callers can hold it across later Appends.
func (*VerifyHistory) ServerIDs ¶ added in v0.49.0
func (h *VerifyHistory) ServerIDs() []string
ServerIDs returns the server ids present in the history, sorted, so a consumer can enumerate what was recorded without access to the server registry (a run's server may since have been removed from it).
type VerifyMode ¶ added in v0.26.0
type VerifyMode string
VerifyMode selects which internal/verify engine path a run uses.
const ( // VerifyModeBaselineAnchored compares each table's last read of the // database with the baseline before it, drift-free — no live source read. // The default. VerifyModeBaselineAnchored VerifyMode = "baseline-anchored" // VerifyModeLiveSource reconstructs each table to a consistent snapshot of // the live source and compares. Needs the server's source DSN and reads // the whole table off production — the console warns to run it off-peak, // matching docs/verify.md. VerifyModeLiveSource VerifyMode = "live-source" // VerifyModeRecoverInputs walks each primary key's event chain and asserts // the before/after images recover consumes are internally consistent — the // console face of `bintrail verify --check recover` (#1191). Index-only: // needs no baseline and no source DSN, which is why the scheduled runner // falls back to it for servers with no baseline configured. VerifyModeRecoverInputs VerifyMode = "recover-inputs" )
type VerifyRequest ¶ added in v0.26.0
type VerifyRequest struct {
ServerID string
ServerName string
Mode VerifyMode
Tables []string // optional "schema.table" filter; empty = all
IndexDSN string
SourceDSN string // only required/used for VerifyModeLiveSource
// SourceSSL is how the live-source check connects to the source: the
// entry's SourceSSL, the TLS capture uses for it.
SourceSSL config.SSL
BaselineDir string
BaselineS3 string
NoArchive bool
}
VerifyRequest is the in-process job description the endpoint hands the controller. The index/source DSNs (secrets) stay inside the process — never written to disk or serialized to any HTTP response.
type VerifyRowDiff ¶ added in v0.26.0
type VerifyRowDiff struct {
PK string `json:"pk"`
Kind string `json:"kind"` // changed | missing | extra
Cells []VerifyCellDiff `json:"cells,omitempty"`
}
VerifyRowDiff is the wire view of verify.RowDiff.
type VerifyRunRecord ¶ added in v0.49.0
type VerifyRunRecord struct {
ServerID string `json:"server_id"`
ServerName string `json:"server_name,omitempty"`
// Trigger records who started the run: "manual" (the POST endpoint) or
// "scheduled" (the watch daemon's --verify-interval loop, #1191).
Trigger string `json:"trigger"`
// SkipReason is set only on records with State "skipped" — a scheduled
// cycle that could not run this server (e.g. a manual run was already in
// flight). Recorded rather than dropped so a schedule that never actually
// verifies is visible in the history, not silent.
SkipReason string `json:"skip_reason,omitempty"`
VerifyStatus
}
VerifyRunRecord is one completed (or skipped) verify run as stored in the history file and served by GET /api/servers/{id}/verify/history. It embeds the same VerifyStatus shape the live status endpoint serves, so a consumer renders a historical run and the current one with the same code.
type VerifyStatus ¶ added in v0.26.0
type VerifyStatus struct {
State string `json:"state"` // idle | running | succeeded | failed (history records may also carry "skipped" — see VerifyRunRecord)
Mode VerifyMode `json:"mode,omitempty"`
Since string `json:"since,omitempty"`
FinishedAt string `json:"finished_at,omitempty"`
LastError string `json:"last_error,omitempty"`
// Note carries a benign, non-error informational message — e.g. "only one
// baseline exists yet, nothing to compare" (a legitimate first run, not a
// failure; mirrors the CLI's zero-exit early return in that case).
Note string `json:"note,omitempty"`
Results []VerifyTableResult `json:"results,omitempty"`
Summary VerifySummary `json:"summary"`
// Verdict is the run-level outcome of a succeeded run (verdict.Verified,
// Mismatch, Error, Differs, Unproven or NoPredecessor), empty for any other state.
// Computed where the status is READ, never stored (WithVerdict, applied by
// VerifyHistory.List and by every handler that serves a status): every
// run already on disk gets it too, and there is one rule for it, the one
// `bintrail verify` exits on. A run that finished without error but
// proved no table is "succeeded" with verdict "unproven", and the page
// must not call it verified.
Verdict string `json:"verdict,omitempty"`
}
VerifyStatus is the pollable state of a server's most recent verify run. Unlike BaselineStatus (one terminal struct overwritten at job end), Results grows as each table completes so the console can show progress mid-run — internal/verify has no progress callback of its own; the controller loops over tables itself and appends after each one returns.
func (VerifyStatus) WithVerdict ¶ added in v0.86.0
func (st VerifyStatus) WithVerdict() VerifyStatus
WithVerdict returns st with Verdict filled for a succeeded run, by the shared rule (verdict.Of), and empty for any other state. Exported because the type reaches embedders through the assurance facade, and a verdict field they could read but not compute would be one that is always empty. A succeeded run with nothing tallied and a Note is the one-baseline case: the supervisor sets a Note for that and nothing else (consoleapp's setNote has one caller), so it is NoPredecessor, as the CLI reports it, not Unproven.
type VerifySummary ¶ added in v0.26.0
type VerifySummary struct {
Match int `json:"match"`
Mismatch int `json:"mismatch"`
Inconclusive int `json:"inconclusive"`
// InconclusiveNothingToCheck mirrors verify.Summary's benign slice of
// Inconclusive (#1416): quiet or append-only tables where zero assertions
// is the expected outcome. Always <= Inconclusive.
InconclusiveNothingToCheck int `json:"inconclusive_nothing_to_check"`
// InconclusiveDiffers mirrors verify.Summary's: inconclusive tables where
// a difference was found, over a snapshot read with no locks. One of
// them makes the run's verdict "differs". Zero on a run recorded before
// the field existed.
InconclusiveDiffers int `json:"inconclusive_differs"`
Error int `json:"error"`
// Total is the number of results tallied — the same `total` the CLI's
// `verify --format json` summary carries.
Total int `json:"total"`
}
VerifySummary tallies VerifyStatus.Results by status, for a run's headline. Its fields mirror verify.Summary EXACTLY — same names, types and order — because consoleapp's supervisor tallies through verify.Summary.Count (the one bucket classification, #1127) and publishes here via a struct conversion. The compiler rejects that conversion when field names, order, or types drift; struct TAGS are ignored by conversion identity, so a JSON tag rename on either side would NOT be caught — keep the tags in sync by hand. It stays a distinct type only because this package must not import internal/verify (the read layer must not link the capture library — see internal/event's dep guard).
type VerifyTableResult ¶ added in v0.26.0
type VerifyTableResult struct {
Schema string `json:"schema"`
Table string `json:"table"`
// Status is normalized through verify.NormalizeStatus before it reaches
// the wire — the same status→bucket decision the CLI's JSON report uses,
// so an unrecognized engine status surfaces as "error", never as a benign
// value (#1127).
Status string `json:"status"` // match | mismatch | inconclusive | error
// Reason is the detail behind the verdict — the same datum, under the same
// name, as the CLI's `verify --format json` per-table `reason`.
Reason string `json:"reason,omitempty"`
// Detail is the legacy alias for Reason, kept for consumers of the
// original #677 wire shape. Always carries the same value as Reason.
Detail string `json:"detail,omitempty"`
// InconclusiveKind subdivides an inconclusive verdict (#1416):
// no-activity | nothing-to-assert | unproven. Empty otherwise.
InconclusiveKind string `json:"inconclusive_kind,omitempty"`
SourceRows int64 `json:"source_rows,omitempty"`
ReconstructRows int64 `json:"reconstruct_rows,omitempty"`
// EventsChecked/ChainsChecked: the recover-inputs walk's per-table
// counters, same tag names as the CLI's verify --format json (#1425
// review: the console rendered a counts column these fields never
// reached — toWireResult dropped them on the floor). Zero-omitted; the
// content modes carry neither.
EventsChecked int `json:"events_checked,omitempty"`
ChainsChecked int `json:"chains_checked,omitempty"`
Anchor string `json:"anchor,omitempty"`
// ComparedTo names the read of the database a baseline-anchored table was
// fingerprinted against (RFC3339, UTC): the same datum, under the same
// name, as the CLI's `verify --format json` compared_to. The newest
// snapshot can be days newer than that read, so a "match" without it reads
// as "the newest snapshot is verified". Empty where no two fingerprints
// were compared.
ComparedTo string `json:"compared_to,omitempty"`
// SnapshotLock is how the snapshots this table was compared with were
// locked when the database was read: consistent | unknown | torn. The same
// datum, under the same name, as the CLI's snapshot_lock. Empty where no
// snapshot was compared.
SnapshotLock string `json:"snapshot_lock,omitempty"`
// Explainable is true only for a baseline-anchored mismatch whose pair is
// still cached from the run that produced this result — the precondition
// for calling Explain on it.
Explainable bool `json:"explainable"`
}
VerifyTableResult is the wire view of one table's verify.TableResult.
Source Files
¶
- access_profiles_api.go
- activity_api.go
- api.go
- assets.go
- audit.go
- auth.go
- auth_api.go
- authfile.go
- authz.go
- backup_schedule.go
- backup_schedule_api.go
- backup_settings_api.go
- backup_settings_store.go
- backup_settings_validate.go
- baseline_history.go
- baseline_location_migration.go
- baseline_trigger.go
- baselinedecimals.go
- baselines_api.go
- baselines_detail.go
- baselines_sources.go
- bucketregion.go
- capacity_api.go
- capture_skips.go
- capture_status_api.go
- configpath.go
- connect.go
- coverage_api.go
- coverage_warnings.go
- cursor.go
- dirwritable_unix.go
- draft.go
- dto.go
- events_head.go
- extsettings.go
- extview.go
- firstrun.go
- flashback.go
- flashback_api.go
- flavor.go
- identify_api.go
- local_copy.go
- manager.go
- mcp.go
- mcptoken.go
- mcptoken_api.go
- monitor.go
- pagenames.go
- profiles_api.go
- ratelimit.go
- reconstruct.go
- recover_cascade.go
- refused_tables.go
- registry.go
- rotation_api.go
- schema_changes_api.go
- schema_snapshot.go
- server.go
- servername.go
- servers_api.go
- session_profile.go
- sessions.go
- shared_location.go
- snapshot_expiry_api.go
- snapshot_failure.go
- snapshot_writers.go
- sql_api.go
- sql_export.go
- storage_api.go
- telemetry_api.go
- uncaptured_api.go
- verify_history.go
- verify_trigger.go
- views_api.go
- views_live.go
- views_skipped.go