Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: build push test
.PHONY: build push test deps

TAG ?= $(shell git log -n 1 --pretty=format:"%H")
IMAGE ?= databack/mysql-backup
Expand All @@ -11,6 +11,10 @@ GOOS?=$(shell uname -s | tr '[:upper:]' '[:lower:]')
GOARCH?=$(shell uname -m)
BIN ?= $(DIST)/mysql-backup-$(GOOS)-$(GOARCH)

API ?= github.com/databacker/api/go/api
APITAG ?= latest


build-docker:
docker buildx build -t $(BUILDIMAGE) --platform $(OCIPLATFORMS) .

Expand Down Expand Up @@ -62,3 +66,6 @@ clean-test-remove:
@echo

clean-test: clean-test-stop clean-test-remove

deps:
go get -u $(API)@$(APITAG)
25 changes: 25 additions & 0 deletions cmd/dump.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,18 @@ func dumpCmd(passedExecs execs, cmdConfig *cmdConfiguration) (*cobra.Command, er
// done with the startup
startupSpan.End()

// Fetch the MySQL server UUID once before the timer loop for use in
// protected-target identity. This is best-effort: if unavailable the
// identity will still be deterministic, just without the server UUID.
var serverUUID string
if cmdConfig.dbconn != nil {
if id, err := cmdConfig.dbconn.ServerUUID(); err == nil {
serverUUID = id
} else {
cmdConfig.logger.WithError(err).Debug("could not retrieve MySQL server_uuid; protected target identity will exclude it")
}
}

if err := executor.Timer(timerOpts, func() error {
// start a new span for the dump, should not be a child of the startup one
tracerCtx, dumpSpan := tracer.Start(ctx, string(api.BackupSpanRun))
Expand All @@ -280,6 +292,18 @@ func dumpCmd(passedExecs execs, cmdConfig *cmdConfiguration) (*cobra.Command, er
}
}
dumpSpan.SetAttributes(attrs...)
// Emit configuration-based protected target attributes on the run span.
// selection_mode and configured_databases reflect how the engine was
// configured; actual databases are emitted later on the dump spans.
ptMode := core.BuildSelectionMode(include, exclude)
var ptConfiguredDBs []string
switch ptMode {
case api.BackupProtectedTargetSelectionModeInclude:
ptConfiguredDBs = include
case api.BackupProtectedTargetSelectionModeExclude:
ptConfiguredDBs = exclude
}
core.SetRunSpanProtectedTargetAttrs(dumpSpan, ptMode, serverUUID, ptConfiguredDBs)
defer func() {
attrs := []attribute.KeyValue{
attribute.String(string(api.BackupAttrStatus), backupStatus),
Expand Down Expand Up @@ -311,6 +335,7 @@ func dumpCmd(passedExecs execs, cmdConfig *cmdConfiguration) (*cobra.Command, er
FilenamePattern: filenamePattern,
Parallelism: parallel,
IgnoreTables: ignoreTables,
ServerUUID: serverUUID,
}
results, err := executor.Dump(tracerCtx, dumpOpts)
if err != nil {
Expand Down
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ require (
filippo.io/age v1.2.1
github.com/InfiniteLoopSpace/go_S-MIME v0.0.0-20181221134359-3f58f9a4b2b6
github.com/bramvdbogaerde/go-scp v1.5.0
github.com/databacker/api/go/api v0.0.0-20260511132103-4edbce2341a3
github.com/databacker/api/go/api v0.0.0-20260603135428-161b2ee82911
github.com/gliderlabs/ssh v0.3.8
github.com/google/go-cmp v0.7.0
github.com/kevinburke/ssh_config v1.2.0
Expand All @@ -55,7 +55,7 @@ require (
github.com/containerd/log v0.1.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-chi/chi/v5 v5.2.5 // indirect
github.com/go-chi/chi/v5 v5.3.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
Expand All @@ -64,7 +64,7 @@ require (
github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/oapi-codegen/runtime v1.4.0 // indirect
github.com/oapi-codegen/runtime v1.4.1 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
Expand Down
29 changes: 11 additions & 18 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,10 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/databacker/api/go/api v0.0.0-20260203120843-ea139d59c2ed h1:PKzk3NUbJGSOpINtzGNSJR8jLqrTIy0enpaUma5RFgk=
github.com/databacker/api/go/api v0.0.0-20260203120843-ea139d59c2ed/go.mod h1:vXp1gX/diWXW659asaBZ7K3Wgs2OG3igz0dF0UB4yIY=
github.com/databacker/api/go/api v0.0.0-20260511123027-25b8d0ecddb7 h1:wAl574i+9CD/SJLh7ng8NuGMGa04VCFPeoxqk9wFRFE=
github.com/databacker/api/go/api v0.0.0-20260511123027-25b8d0ecddb7/go.mod h1:vXp1gX/diWXW659asaBZ7K3Wgs2OG3igz0dF0UB4yIY=
github.com/databacker/api/go/api v0.0.0-20260511131506-50108473a7ba h1:vMkvDnSGi3QzJPSs9zEowfUHSt2U2nXgWmSZafLzRyw=
github.com/databacker/api/go/api v0.0.0-20260511131506-50108473a7ba/go.mod h1:vXp1gX/diWXW659asaBZ7K3Wgs2OG3igz0dF0UB4yIY=
github.com/databacker/api/go/api v0.0.0-20260511132103-4edbce2341a3 h1:SVRt6Uqajczc09iEGcax2AhL16dYrvcjanX81TcWROM=
github.com/databacker/api/go/api v0.0.0-20260511132103-4edbce2341a3/go.mod h1:vXp1gX/diWXW659asaBZ7K3Wgs2OG3igz0dF0UB4yIY=
github.com/databacker/api/go/api v0.0.0-20260603133243-b6e190c1ffbc h1:l+AysIwoI7nT0K7WDDa5bHRhtaHgL00Gs11D6yWEd1E=
github.com/databacker/api/go/api v0.0.0-20260603133243-b6e190c1ffbc/go.mod h1:vXp1gX/diWXW659asaBZ7K3Wgs2OG3igz0dF0UB4yIY=
github.com/databacker/api/go/api v0.0.0-20260603135428-161b2ee82911 h1:MN97c3fLpKuVjLo4wal9KY4uHKHm4cSZnlz7YiNuDwU=
github.com/databacker/api/go/api v0.0.0-20260603135428-161b2ee82911/go.mod h1:vXp1gX/diWXW659asaBZ7K3Wgs2OG3igz0dF0UB4yIY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand All @@ -122,10 +118,8 @@ github.com/geoffgarside/ber v1.1.0/go.mod h1:jVPKeCbj6MvQZhwLYsGwaGI52oUorHoHKNe
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-chi/chi/v5 v5.2.4 h1:WtFKPHwlywe8Srng8j2BhOD9312j9cGUxG1SP4V2cR4=
github.com/go-chi/chi/v5 v5.2.4/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM=
github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
Expand Down Expand Up @@ -226,10 +220,10 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQgXIyI=
github.com/oapi-codegen/runtime v1.1.2/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
github.com/oapi-codegen/runtime v1.4.0 h1:KLOSFOp7UzkbS7Cs1ms6NBEKYr0WmH2wZG0KKbd2er4=
github.com/oapi-codegen/runtime v1.4.0/go.mod h1:5sw5fxCDmnOzKNYmkVNF8d34kyUeejJEY8HNT2WaPec=
github.com/oapi-codegen/nullable v1.1.0 h1:eAh8JVc5430VtYVnq00Hrbpag9PFRGWLjxR1/3KntMs=
github.com/oapi-codegen/nullable v1.1.0/go.mod h1:KUZ3vUzkmEKY90ksAmit2+5juDIhIZhfDl+0PwOQlFY=
github.com/oapi-codegen/runtime v1.4.1 h1:9nwLoI+KrWxzbBcp0jO/R8uXqbik/HUyCvPeU68Y/qo=
github.com/oapi-codegen/runtime v1.4.1/go.mod h1:GwV7hC2hviaMzj+ITfHVRESK5J2W/GefVwIND/bMGvU=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
Expand Down Expand Up @@ -442,9 +436,8 @@ golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
Expand Down
12 changes: 12 additions & 0 deletions pkg/core/dump.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,17 @@ func (e *Executor) Dump(ctx context.Context, opts DumpOptions) (DumpResults, err
// filter out excluded databases
dbnames = filterExcludedDatabases(dbnames, opts.Exclude)
span.SetAttributes(attribute.StringSlice(string(api.BackupAttrActualSchemas), dbnames))

// emit protected target attributes on the dump span now that the actual
// database list is known. The identity is built from the configured include/
// exclude lists so it is stable regardless of runtime schema discovery.
ptMode := BuildSelectionMode(opts.DBNames, opts.Exclude)
ptConfiguredDBs := opts.DBNames // include list for include mode
if ptMode == api.BackupProtectedTargetSelectionModeExclude {
ptConfiguredDBs = opts.Exclude
}
SetDumpSpanProtectedTargetAttrs(span, ptMode, opts.ServerUUID, ptConfiguredDBs, dbnames)

for _, s := range dbnames {
outFile := path.Join(workdir, fmt.Sprintf("%s_%s.sql", s, timepart))
f, err := os.Create(outFile)
Expand All @@ -110,6 +121,7 @@ func (e *Executor) Dump(ctx context.Context, opts DumpOptions) (DumpResults, err
}
results.DumpStart = time.Now()
dbDumpCtx, dbDumpSpan := tracer.Start(ctx, string(api.BackupSpanDatabaseDump))
SetDumpSpanProtectedTargetAttrs(dbDumpSpan, ptMode, opts.ServerUUID, ptConfiguredDBs, dbnames)
if err := database.Dump(dbDumpCtx, dbconn, database.DumpOpts{
Compact: compact,
Triggers: triggers,
Expand Down
3 changes: 3 additions & 0 deletions pkg/core/dumpoptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,7 @@ type DumpOptions struct {
// Parallelism how many databases to back up at once, consuming that number of threads
Parallelism int
IgnoreTables []string
// ServerUUID is the MySQL server's @@global.server_uuid, used to build the
// protected_target.identity span attribute. May be empty if unavailable.
ServerUUID string
}
102 changes: 102 additions & 0 deletions pkg/core/protectedtarget.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package core

import (
"encoding/json"
"sort"
"strings"

"github.com/databacker/api/go/api"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)

// BuildSelectionMode derives the selection mode from the configured include and
// exclude lists:
//
// include — one or more databases are explicitly named for inclusion.
// exclude — all databases except a named subset (include is empty, exclude non-empty).
// all — entire server/cluster (both lists are empty).
func BuildSelectionMode(include, exclude []string) api.BackupProtectedTargetSelectionMode {
if len(include) > 0 {
return api.BackupProtectedTargetSelectionModeInclude
}
if len(exclude) > 0 {
return api.BackupProtectedTargetSelectionModeExclude
}
return api.BackupProtectedTargetSelectionModeAll
}

// BuildProtectedTargetIdentity produces a stable, deterministic identity string
// for a MySQL protected target.
//
// Format: mysql:{mode}:{native_id}[:{sorted_db1}:{sorted_db2}:...]
//
// For include and exclude modes, the sorted configured database names are
// appended as additional colon-separated components. nativeID may be empty
// if the server UUID could not be retrieved.
//
// The same configured target backed up by different engine instances will
// produce the same identity when connected to the same MySQL server with the
// same selection mode and configured database list.
func BuildProtectedTargetIdentity(mode api.BackupProtectedTargetSelectionMode, nativeID string, configuredDBNames []string) string {
parts := []string{"mysql", string(mode), nativeID}
if (mode == api.BackupProtectedTargetSelectionModeInclude || mode == api.BackupProtectedTargetSelectionModeExclude) && len(configuredDBNames) > 0 {
sorted := make([]string, len(configuredDBNames))
copy(sorted, configuredDBNames)
sort.Strings(sorted)
parts = append(parts, sorted...)
}
return strings.Join(parts, ":")
}

// SetRunSpanProtectedTargetAttrs sets the configuration-based protected-target
// attributes on the root run span:
//
// backup.protected_target.selection_mode — how databases are selected
// backup.protected_target.configured_databases — the include/exclude list (omitted for all mode)
// backup.protected_target.identity — stable deterministic identity
// db.server.native_id — MySQL server_uuid (when available)
func SetRunSpanProtectedTargetAttrs(span trace.Span, mode api.BackupProtectedTargetSelectionMode, nativeID string, configuredDBNames []string) {
identity := BuildProtectedTargetIdentity(mode, nativeID, configuredDBNames)
attrs := []attribute.KeyValue{
attribute.String(string(api.BackupAttrProtectedTargetSelectionMode), string(mode)),
attribute.String(string(api.BackupAttrProtectedTargetIdentity), identity),
}
if nativeID != "" {
attrs = append(attrs, attribute.String(string(api.BackupAttrDBServerNativeID), nativeID))
}
if (mode == api.BackupProtectedTargetSelectionModeInclude || mode == api.BackupProtectedTargetSelectionModeExclude) && len(configuredDBNames) > 0 {
sorted := make([]string, len(configuredDBNames))
copy(sorted, configuredDBNames)
sort.Strings(sorted)
dbJSON, _ := json.Marshal(sorted)
attrs = append(attrs, attribute.String(string(api.BackupAttrProtectedTargetConfiguredDatabases), string(dbJSON)))
}
span.SetAttributes(attrs...)
}

// SetDumpSpanProtectedTargetAttrs sets protected-target attributes on dump and
// database_dump spans. It emits selection_mode, identity, and native_id
// (derived from the configuration) plus the actual database_count and databases
// resolved at runtime after schema discovery and exclude-list filtering.
func SetDumpSpanProtectedTargetAttrs(span trace.Span, mode api.BackupProtectedTargetSelectionMode, nativeID string, configuredDBNames []string, actualDBNames []string) {
identity := BuildProtectedTargetIdentity(mode, nativeID, configuredDBNames)
attrs := []attribute.KeyValue{
attribute.String(string(api.BackupAttrProtectedTargetSelectionMode), string(mode)),
attribute.String(string(api.BackupAttrProtectedTargetIdentity), identity),
}
if nativeID != "" {
attrs = append(attrs, attribute.String(string(api.BackupAttrDBServerNativeID), nativeID))
}
if len(actualDBNames) > 0 {
sorted := make([]string, len(actualDBNames))
copy(sorted, actualDBNames)
sort.Strings(sorted)
dbJSON, _ := json.Marshal(sorted)
attrs = append(attrs,
attribute.Int(string(api.BackupAttrProtectedTargetDatabaseCount), len(sorted)),
attribute.String(string(api.BackupAttrProtectedTargetDatabases), string(dbJSON)),
)
}
span.SetAttributes(attrs...)
}
18 changes: 18 additions & 0 deletions pkg/database/serverinfo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package database

import "fmt"

// ServerUUID queries MySQL for the server's global server_uuid variable.
// This is a stable identifier for the MySQL server instance across restarts
// (@@global.server_uuid).
func (c *Connection) ServerUUID() (string, error) {
db, err := c.MySQL()
if err != nil {
return "", fmt.Errorf("failed to open connection to database: %v", err)
}
var serverUUID string
if err := db.QueryRow("SELECT @@global.server_uuid").Scan(&serverUUID); err != nil {
return "", fmt.Errorf("failed to query server_uuid: %v", err)
}
return serverUUID, nil
}
Loading