Pulse/internal/monitoring/docker_metadata_migration.go
rcourtman 9d8f8b45b5 fix(docker,metrics): preserve container metadata on update and reduce DB writes
Docker container URL preserved on update (#1054): container updates
recreate the container with a new runtime ID. The agent now includes
{oldContainerId, newContainerId} in the completion ACK payload; the
server uses this to copy persisted metadata (custom URLs, descriptions,
tags) to the new ID so nothing is lost. Migration is a copy, not a move,
so rollback scenarios still find metadata under the original ID.

Reduce metrics.db write amplification (#1124): add a UNIQUE index on
(resource_type, resource_id, metric_type, timestamp, tier) so rollup
reprocessing after a failed checkpoint uses INSERT OR IGNORE instead of
creating duplicate rows. Existing duplicates are deduplicated once on
startup if the index creation would otherwise fail. Also sets
wal_autocheckpoint(500) to checkpoint the WAL more frequently, preventing
unbounded WAL growth.

Fixes #1054
Fixes #1124
2026-02-18 12:56:46 +00:00

72 lines
2.3 KiB
Go

package monitoring
import (
"fmt"
"slices"
"strings"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
)
// CopyDockerContainerMetadata copies persisted container metadata from an old container runtime ID to a new one.
//
// Docker container updates typically recreate the container, producing a new runtime ID. Persisted metadata
// (custom URL, description, tags, notes) is keyed by resource ID and would otherwise be "lost" for the new
// container.
//
// This is intentionally a copy (not a move) so rollback-to-backup scenarios can still find metadata under
// the original container ID.
func (m *Monitor) CopyDockerContainerMetadata(hostID, oldContainerID, newContainerID string) error {
if m == nil || m.dockerMetadataStore == nil {
return nil
}
hostID = strings.TrimSpace(hostID)
oldContainerID = strings.TrimSpace(oldContainerID)
newContainerID = strings.TrimSpace(newContainerID)
if hostID == "" || oldContainerID == "" || newContainerID == "" || oldContainerID == newContainerID {
return nil
}
oldKey := fmt.Sprintf("%s:container:%s", hostID, oldContainerID)
newKey := fmt.Sprintf("%s:container:%s", hostID, newContainerID)
oldMeta := m.dockerMetadataStore.Get(oldKey)
if oldMeta == nil {
return nil
}
if oldMeta.CustomURL == "" && oldMeta.Description == "" && len(oldMeta.Tags) == 0 && len(oldMeta.Notes) == 0 {
return nil
}
newMeta := m.dockerMetadataStore.Get(newKey)
var merged config.DockerMetadata
if newMeta != nil {
merged = *newMeta
}
// Merge missing fields from old -> new, so we don't clobber any metadata already present under the new ID.
if merged.CustomURL == "" {
merged.CustomURL = oldMeta.CustomURL
}
if merged.Description == "" {
merged.Description = oldMeta.Description
}
if len(merged.Tags) == 0 && len(oldMeta.Tags) > 0 {
merged.Tags = append([]string(nil), oldMeta.Tags...)
}
if len(merged.Notes) == 0 && len(oldMeta.Notes) > 0 {
merged.Notes = append([]string(nil), oldMeta.Notes...)
}
// Avoid an unnecessary disk write if nothing changed.
if newMeta != nil &&
merged.CustomURL == newMeta.CustomURL &&
merged.Description == newMeta.Description &&
slices.Equal(merged.Tags, newMeta.Tags) &&
slices.Equal(merged.Notes, newMeta.Notes) {
return nil
}
return m.dockerMetadataStore.Set(newKey, &merged)
}