BuildKeysFromRecord omits alias metadata, leaving stale alias keys undeleted #690

Closed
opened 2026-03-28 04:27:28 +00:00 by mfreeman451 · 1 comment
Owner

Imported from GitHub.

Original GitHub issue: #2152
Original author: @mfreeman451
Original URL: https://github.com/carverauto/serviceradar/issues/2152
Original created: 2025-12-16T05:18:46Z


Summary

  • Context: The identity map system stores canonical device records in a KV store and maintains multiple lookup keys (by IP, MAC, device ID, etc.) that point to the same canonical record. When a device update occurs, the system creates new keys and removes stale keys that are no longer valid.
  • Bug: BuildKeysFromRecord fails to reconstruct alias-related identity keys (from _alias_last_seen_service_id, _alias_last_seen_ip, service_alias:*, and ip_alias:* metadata fields) that were originally created by BuildKeys.
  • Actual vs. expected: When the identity publisher retrieves an existing record to identify stale keys for deletion, it uses BuildKeysFromRecord to reconstruct the keys that should exist. Since BuildKeysFromRecord omits alias keys, these keys are never identified as stale and are never deleted, even when they should be.
  • Impact: Stale alias keys accumulate in the KV store indefinitely, causing the KV store to grow without bound. This undermines the purpose of the stale key deletion mechanism introduced in commit 3a5787ac ("stop KV from growing out of control").

Code with bug

In pkg/identitymap/identitymap.go, the BuildKeysFromRecord function only reconstructs a subset of metadata fields:

// BuildKeysFromRecord reconstructs the identity keys for a canonical record.
func BuildKeysFromRecord(record *Record) []Key {
	if record == nil {
		return nil
	}

	update := &models.DeviceUpdate{
		DeviceID:  record.CanonicalDeviceID,
		Partition: record.Partition,
	}

	if record.Attributes != nil {
		if ip := strings.TrimSpace(record.Attributes["ip"]); ip != "" {
			update.IP = ip
		}
		if mac := strings.TrimSpace(record.Attributes["mac"]); mac != "" {
			macUpper := strings.ToUpper(mac)
			update.MAC = &macUpper
		}

		metaKeys := []string{"armis_device_id", "integration_id", "integration_type", "netbox_device_id"}
		for _, key := range metaKeys {
			if val := strings.TrimSpace(record.Attributes[key]); val != "" {
				if update.Metadata == nil {
					update.Metadata = make(map[string]string)
				}
				update.Metadata[key] = val  // <-- BUG 🔴 Missing alias metadata fields
			}
		}
	}

	return BuildKeys(update)
}

The metaKeys list on line 151 does not include any of the alias-related metadata fields that BuildKeys uses (lines 97-119):

  • _alias_last_seen_service_id
  • _alias_last_seen_ip
  • Keys with prefix service_alias:
  • Keys with prefix ip_alias:

Evidence

Example

Consider a device update with alias metadata:

update := &models.DeviceUpdate{
    DeviceID:  "tenant-a:host-device",
    IP:        "10.0.0.5",
    Partition: "tenant-a",
    Metadata: map[string]string{
        "_alias_last_seen_service_id":           "serviceradar:agent:k8s-agent",
        "_alias_last_seen_ip":                   "10.0.0.8",
        "service_alias:serviceradar:poller:k8s": "2025-11-03T15:00:00Z",
        "ip_alias:10.0.0.9":                     "2025-11-03T15:00:00Z",
    },
}

When BuildKeys(update) is called, it creates 13 keys including:

  1. KindDeviceID: "tenant-a:host-device" (the main device ID)
  2. KindDeviceID: "serviceradar:agent:k8s-agent" (from _alias_last_seen_service_id)
  3. KindDeviceID: "serviceradar:poller:k8s" (from service_alias:serviceradar:poller:k8s)
  4. KindIP: "10.0.0.5" (main IP)
  5. KindIP: "10.0.0.8" (from _alias_last_seen_ip)
  6. KindIP: "10.0.0.9" (from ip_alias:10.0.0.9)
  7. KindPartitionIP: "tenant-a:10.0.0.5"
  8. KindPartitionIP: "tenant-a:10.0.0.8"
  9. KindPartitionIP: "tenant-a:10.0.0.9"

These keys are stored in the KV store pointing to the canonical record.

However, when the record is later retrieved and BuildKeysFromRecord is called (in identity_publisher.go:521), only 3 keys are reconstructed:

  1. KindDeviceID: "tenant-a:host-device"
  2. KindIP: "10.0.0.5"
  3. KindPartitionIP: "tenant-a:10.0.0.5"

The 6 alias-related keys are missing from the reconstruction.

Inconsistency within the codebase

Reference code: BuildKeys in pkg/identitymap/identitymap.go (lines 97-119)

if aliasService := strings.TrimSpace(update.Metadata["_alias_last_seen_service_id"]); aliasService != "" {
    add(KindDeviceID, aliasService)
}
if aliasIP := strings.TrimSpace(update.Metadata["_alias_last_seen_ip"]); aliasIP != "" {
    add(KindIP, aliasIP)
    add(KindPartitionIP, partitionIPValue(update.Partition, aliasIP))
}

for key := range update.Metadata {
    switch {
    case strings.HasPrefix(key, "service_alias:"):
        aliasID := strings.TrimSpace(strings.TrimPrefix(key, "service_alias:"))
        if aliasID != "" {
            add(KindDeviceID, aliasID)
        }
    case strings.HasPrefix(key, "ip_alias:"):
        aliasIP := strings.TrimSpace(strings.TrimPrefix(key, "ip_alias:"))
        if aliasIP != "" {
            add(KindIP, aliasIP)
            add(KindPartitionIP, partitionIPValue(update.Partition, aliasIP))
        }
    }
}

Current code: BuildKeysFromRecord in pkg/identitymap/identitymap.go (lines 131-163)

func BuildKeysFromRecord(record *Record) []Key {
    if record == nil {
        return nil
    }

    update := &models.DeviceUpdate{
        DeviceID:  record.CanonicalDeviceID,
        Partition: record.Partition,
    }

    if record.Attributes != nil {
        if ip := strings.TrimSpace(record.Attributes["ip"]); ip != "" {
            update.IP = ip
        }
        if mac := strings.TrimSpace(record.Attributes["mac"]); mac != "" {
            macUpper := strings.ToUpper(mac)
            update.MAC = &macUpper
        }

        metaKeys := []string{"armis_device_id", "integration_id", "integration_type", "netbox_device_id"}
        for _, key := range metaKeys {
            if val := strings.TrimSpace(record.Attributes[key]); val != "" {
                if update.Metadata == nil {
                    update.Metadata = make(map[string]string)
                }
                update.Metadata[key] = val
            }
        }
    }

    return BuildKeys(update)
}

Contradiction

BuildKeys processes four categories of alias metadata fields to create identity keys:

  1. _alias_last_seen_service_id → creates a KindDeviceID key
  2. _alias_last_seen_ip → creates KindIP and KindPartitionIP keys
  3. service_alias:* prefixed keys → creates KindDeviceID keys
  4. ip_alias:* prefixed keys → creates KindIP and KindPartitionIP keys

However, BuildKeysFromRecord only reconstructs metadata for armis_device_id, integration_id, integration_type, and netbox_device_id. None of the alias-related fields are included in the reconstruction, causing BuildKeys to produce a different set of keys when called from BuildKeysFromRecord versus when called directly with the original update.

This violates the documented purpose of BuildKeysFromRecord: to "reconstruct the identity keys for a canonical record" (line 131). The function does not actually reconstruct all the keys that were originally created.

Failing test

Test script

package identitymap

import (
	"testing"

	"github.com/carverauto/serviceradar/pkg/models"
	"github.com/stretchr/testify/assert"
)

// TestBuildKeysFromRecordIncludesAliasKeys verifies that BuildKeysFromRecord
// reconstructs all keys that were originally created by BuildKeys, including
// alias-related keys.
//
// This test currently fails, demonstrating a bug where alias keys are lost
// during record reconstruction.
func TestBuildKeysFromRecordIncludesAliasKeys(t *testing.T) {
	mac := "aa:bb:cc:dd:ee:ff"
	update := &models.DeviceUpdate{
		DeviceID:  "tenant-a:host-device",
		IP:        "10.0.0.5",
		Partition: "tenant-a",
		MAC:       &mac,
		Metadata: map[string]string{
			"_alias_last_seen_service_id":           "serviceradar:agent:k8s-agent",
			"_alias_last_seen_ip":                   "10.0.0.8",
			"service_alias:serviceradar:poller:k8s": "2025-11-03T15:00:00Z",
			"ip_alias:10.0.0.9":                     "2025-11-03T15:00:00Z",
			"armis_device_id":                       "armis-123",
			"integration_type":                      "netbox",
			"integration_id":                        "nb-42",
			"netbox_device_id":                      "123",
		},
	}

	// Create a record as it would be stored (mimicking buildIdentityAttributes)
	record := &Record{
		CanonicalDeviceID: update.DeviceID,
		Partition:         update.Partition,
		MetadataHash:      HashIdentityMetadata(update),
		Attributes: map[string]string{
			"ip":               update.IP,
			"mac":              "AA:BB:CC:DD:EE:FF",
			"armis_device_id":  update.Metadata["armis_device_id"],
			"integration_id":   update.Metadata["integration_id"],
			"integration_type": update.Metadata["integration_type"],
			"netbox_device_id": update.Metadata["netbox_device_id"],
			// Note: alias metadata is NOT stored in attributes by buildIdentityAttributes
		},
	}

	keysFromUpdate := BuildKeys(update)
	keysFromRecord := BuildKeysFromRecord(record)

	// This assertion will fail because BuildKeysFromRecord doesn't reconstruct
	// alias keys (service_alias:*, ip_alias:*, _alias_last_seen_service_id, _alias_last_seen_ip)
	assert.ElementsMatch(t, keysFromUpdate, keysFromRecord,
		"BuildKeysFromRecord should reconstruct all keys that BuildKeys originally created")

	// Specifically verify the alias keys are present
	aliasServiceKey := Key{Kind: KindDeviceID, Value: "serviceradar:agent:k8s-agent"}
	assert.Contains(t, keysFromRecord, aliasServiceKey,
		"Should include alias service ID from _alias_last_seen_service_id")

	aliasServiceKey2 := Key{Kind: KindDeviceID, Value: "serviceradar:poller:k8s"}
	assert.Contains(t, keysFromRecord, aliasServiceKey2,
		"Should include alias service ID from service_alias: prefix")

	aliasIPKey := Key{Kind: KindIP, Value: "10.0.0.8"}
	assert.Contains(t, keysFromRecord, aliasIPKey,
		"Should include alias IP from _alias_last_seen_ip")

	aliasIPKey2 := Key{Kind: KindIP, Value: "10.0.0.9"}
	assert.Contains(t, keysFromRecord, aliasIPKey2,
		"Should include alias IP from ip_alias: prefix")
}

Test output

=== RUN   TestBuildKeysFromRecordIncludesAliasKeys
    alias_keys_bug_test.go:56:
        	Error Trace:	/home/user/serviceradar/pkg/identitymap/alias_keys_bug_test.go:56
        	Error:      	elements differ

        	            	extra elements in list A:
        	            	([]interface {}) (len=6) {
        	            	 (identitymap.Key) {
        	            	  Kind: (identitymappb.IdentityKind) 1,
        	            	  Value: (string) (len=28) "serviceradar:agent:k8s-agent"
        	            	 },
        	            	 (identitymap.Key) {
        	            	  Kind: (identitymappb.IdentityKind) 5,
        	            	  Value: (string) (len=8) "10.0.0.8"
        	            	 },
        	            	 (identitymap.Key) {
        	            	  Kind: (identitymappb.IdentityKind) 6,
        	            	  Value: (string) (len=17) "tenant-a:10.0.0.8"
        	            	 },
        	            	 (identitymap.Key) {
        	            	  Kind: (identitymappb.IdentityKind) 5,
        	            	  Value: (string) (len=8) "10.0.0.9"
        	            	 },
        	            	 (identitymap.Key) {
        	            	  Kind: (identitymappb.IdentityKind) 6,
        	            	  Value: (string) (len=17) "tenant-a:10.0.0.9"
        	            	 },
        	            	 (identitymap.Key) {
        	            	  Kind: (identitymappb.IdentityKind) 1,
        	            	  Value: (string) (len=23) "serviceradar:poller:k8s"
        	            	 }
        	            	}


        	            	listA:
        	            	([]identitymap.Key) (len=13) {
        	            	 (identitymap.Key) {
        	            	  Kind: (identitymappb.IdentityKind) 1,
        	            	  Value: (string) (len=20) "tenant-a:host-device"
        	            	 },
        	            	 (identitymap.Key) {
        	            	  Kind: (identitymappb.IdentityKind) 5,
        	            	  Value: (string) (len=8) "10.0.0.5"
        	            	 },
        	            	 (identitymap.Key) {
        	            	  Kind: (identitymappb.IdentityKind) 6,
        	            	  Value: (string) (len=17) "tenant-a:10.0.0.5"
        	            	 },
        	            	 (identitymap.Key) {
        	            	  Kind: (identitymappb.IdentityKind) 2,
        	            	  Value: (string) (len=9) "armis-123"
        	            	 },
        	            	 (identitymap.Key) {
        	            	  Kind: (identitymappb.IdentityKind) 3,
        	            	  Value: (string) (len=5) "nb-42"
        	            	 },
        	            	 (identitymap.Key) {
        	            	  Kind: (identitymappb.IdentityKind) 3,
        	            	  Value: (string) (len=3) "123"
        	            	 },
        	            	 (identitymap.Key) {
        	            	  Kind: (identitymappb.IdentityKind) 1,
        	            	  Value: (string) (len=28) "serviceradar:agent:k8s-agent"
        	            	 },
        	            	 (identitymap.Key) {
        	            	  Kind: (identitymappb.IdentityKind) 5,
        	            	  Value: (string) (len=8) "10.0.0.8"
        	            	 },
        	            	 (identitymap.Key) {
        	            	  Kind: (identitymappb.IdentityKind) 6,
        	            	  Value: (string) (len=17) "tenant-a:10.0.0.8"
        	            	 },
        	            	 (identitymap.Key) {
        	            	  Kind: (identitymappb.IdentityKind) 5,
        	            	  Value: (string) (len=8) "10.0.0.9"
        	            	 },
        	            	 (identitymap.Key) {
        	            	  Kind: (identitymappb.IdentityKind) 6,
        	            	  Value: (string) (len=17) "tenant-a:10.0.0.9"
        	            	 },
        	            	 (identitymap.Key) {
        	            	  Kind: (identitymappb.IdentityKind) 1,
        	            	  Value: (string) (len=23) "serviceradar:poller:k8s"
        	            	 },
        	            	 (identitymap.Key) {
        	            	  Kind: (identitymappb.IdentityKind) 4,
        	            	  Value: (string) (len=17) "AA:BB:CC:DD:EE:FF"
        	            	 }
        	            	}


        	            	listB:
        	            	([]identitymap.Key) (len=7) {
        	            	 (identitymap.Key) {
        	            	  Kind: (identitymappb.IdentityKind) 1,
        	            	  Value: (string) (len=20) "tenant-a:host-device"
        	            	 },
        	            	 (identitymap.Key) {
        	            	  Kind: (identitymappb.IdentityKind) 5,
        	            	  Value: (string) (len=8) "10.0.0.5"
        	            	 },
        	            	 (identitymap.Key) {
        	            	  Kind: (identitymappb.IdentityKind) 6,
        	            	  Value: (string) (len=17) "tenant-a:10.0.0.5"
        	            	 },
        	            	 (identitymap.Key) {
        	            	  Kind: (identitymappb.IdentityKind) 2,
        	            	  Value: (string) (len=9) "armis-123"
        	            	 },
        	            	 (identitymap.Key) {
        	            	  Kind: (identitymappb.IdentityKind) 3,
        	            	  Value: (string) (len=5) "nb-42"
        	            	 },
        	            	 (identitymap.Key) {
        	            	  Kind: (identitymappb.IdentityKind) 3,
        	            	  Value: (string) (len=3) "123"
        	            	 },
        	            	 (identitymap.Key) {
        	            	  Kind: (identitymappb.IdentityKind) 4,
        	            	  Value: (string) (len=17) "AA:BB:CC:DD:EE:FF"
        	            	 }
        	            	}
        	Test:       	TestBuildKeysFromRecordIncludesAliasKeys
        	Messages:   	BuildKeysFromRecord should reconstruct all keys that BuildKeys originally created
    alias_keys_bug_test.go:61:
        	Error Trace:	/home/user/serviceradar/pkg/identitymap/alias_keys_bug_test.go:61
        	Error:      	[]identitymap.Key{identitymap.Key{Kind:1, Value:"tenant-a:host-device"}, identitymap.Key{Kind:5, Value:"10.0.0.5"}, identitymap.Key{Kind:6, Value:"tenant-a:10.0.0.5"}, identitymap.Key{Kind:2, Value:"armis-123"}, identitymap.Key{Kind:3, Value:"nb-42"}, identitymap.Key{Kind:3, Value:"123"}, identitymap.Key{Kind:4, Value:"AA:BB:CC:DD:EE:FF"}} does not contain identitymap.Key{Kind:1, Value:"serviceradar:agent:k8s-agent"}
        	Test:       	TestBuildKeysFromRecordIncludesAliasKeys
        	Messages:   	Should include alias service ID from _alias_last_seen_service_id
    alias_keys_bug_test.go:65:
        	Error Trace:	/home/user/serviceradar/pkg/identitymap/alias_keys_bug_test.go:65
        	Error:      	[]identitymap.Key{identitymap.Key{Kind:1, Value:"tenant-a:host-device"}, identitymap.Key{Kind:5, Value:"10.0.0.5"}, identitymap.Key{Kind:6, Value:"tenant-a:10.0.0.5"}, identitymap.Key{Kind:2, Value:"armis-123"}, identitymap.Key{Kind:3, Value:"nb-42"}, identitymap.Key{Kind:3, Value:"123"}, identitymap.Key{Kind:4, Value:"AA:BB:CC:DD:EE:FF"}} does not contain identitymap.Key{Kind:1, Value:"serviceradar:poller:k8s"}
        	Test:       	TestBuildKeysFromRecordIncludesAliasKeys
        	Messages:   	Should include alias service ID from service_alias: prefix
    alias_keys_bug_test.go:69:
        	Error Trace:	/home/user/serviceradar/pkg/identitymap/alias_keys_bug_test.go:69
        	Error:      	[]identitymap.Key{identitymap.Key{Kind:1, Value:"tenant-a:host-device"}, identitymap.Key{Kind:5, Value:"10.0.0.5"}, identitymap.Key{Kind:6, Value:"tenant-a:10.0.0.5"}, identitymap.Key{Kind:2, Value:"armis-123"}, identitymap.Key{Kind:3, Value:"nb-42"}, identitymap.Key{Kind:3, Value:"123"}, identitymap.Key{Kind:4, Value:"AA:BB:CC:DD:EE:FF"}} does not contain identitymap.Key{Kind:5, Value:"10.0.0.8"}
        	Test:       	TestBuildKeysFromRecordIncludesAliasKeys
        	Messages:   	Should include alias IP from _alias_last_seen_ip
    alias_keys_bug_test.go:73:
        	Error Trace:	/home/user/serviceradar/pkg/identitymap/alias_keys_bug_test.go:73
        	Error:      	[]identitymap.Key{identitymap.Key{Kind:1, Value:"tenant-a:host-device"}, identitymap.Key{Kind:5, Value:"10.0.0.5"}, identitymap.Key{Kind:6, Value:"tenant-a:10.0.0.5"}, identitymap.Key{Kind:2, Value:"armis-123"}, identitymap.Key{Kind:3, Value:"nb-42"}, identitymap.Key{Kind:3, Value:"123"}, identitymap.Key{Kind:4, Value:"AA:BB:CC:DD:EE:FF"}} does not contain identitymap.Key{Kind:5, Value:"10.0.0.9"}
        	Test:       	TestBuildKeysFromRecordIncludesAliasKeys
        	Messages:   	Should include alias IP from ip_alias: prefix
--- FAIL: TestBuildKeysFromRecordIncludesAliasKeys (0.00s)
FAIL
FAIL	github.com/carverauto/serviceradar/pkg/identitymap	0.006s
FAIL

The test clearly shows that BuildKeysFromRecord produces only 7 keys while BuildKeys produces 13 keys for the same device. The 6 missing keys are all alias-related keys.

Full context

The identity map system is used to maintain a canonical mapping of device identities across the ServiceRadar platform. When a device is discovered or updated, multiple identity keys are created (by IP address, MAC address, device ID, integration IDs, etc.) that all point to the same canonical device record stored in a NATS JetStream KV store.

The identityPublisher in pkg/registry/identity_publisher.go is responsible for publishing these mappings to the KV store. When publishing an update for a device:

  1. It calls BuildKeys(update) to generate all identity keys for the new update (lines 216-222)
  2. It retrieves the existing canonical record from the KV store to find what keys currently exist (line 206, which calls existingIdentitySnapshot)
  3. Inside existingIdentitySnapshot, it calls BuildKeysFromRecord(record) to reconstruct what keys should exist based on the stored record (line 521)
  4. It compares the new key set with the reconstructed old key set to identify stale keys (line 225)
  5. It deletes the stale keys from the KV store (lines 226-228)

This stale key deletion mechanism was explicitly added in commit 3a5787ac on Oct 16, 2025 to "stop KV from growing out of control."

However, on Nov 4, 2025 (commit 5223ac8c), alias support was added to BuildKeys to track device aliases (when a device is known by multiple service IDs or IP addresses). This allows the system to create additional lookup keys for aliased identities. The commit added support for:

  • _alias_last_seen_service_id: The last service ID this device was seen as
  • _alias_last_seen_ip: The last IP address this device was seen at
  • service_alias:*: Historical service IDs this device has been known as
  • ip_alias:*: Historical IP addresses this device has been associated with

The problem is that BuildKeysFromRecord was not updated to handle these alias fields. Additionally, the buildIdentityAttributes function in pkg/registry/identity_publisher.go (lines 408-450) does not store alias metadata in the record's attributes, so even if BuildKeysFromRecord tried to reconstruct them, the data wouldn't be available.

This means:

  1. When a device with aliases is published, alias keys are created in the KV store
  2. When the same device is later updated (even if aliases change or are removed), the old alias keys are never identified as stale
  3. These orphaned alias keys remain in the KV store indefinitely, accumulating over time
  4. The KV store grows without bound, exactly the problem that commit 3a5787ac was meant to prevent

The device alias feature is actively used in the codebase (see pkg/core/alias_events.go, pkg/devicealias/alias.go) to track when devices are seen under different identities, which is an important feature for device tracking in complex networks.

Why has this bug gone undetected?

This bug has gone undetected for several reasons:

  1. Silent failure: The bug doesn't cause any errors or exceptions. The identity publisher successfully creates the alias keys and successfully publishes updates. The only symptom is that stale keys are not deleted, which is not immediately observable.

  2. Gradual accumulation: The effect of the bug is a gradual accumulation of stale keys over time. In a test environment or during initial deployment, the KV store might not grow large enough to be noticed. Only in production with sustained use would the KV store growth become apparent.

  3. Temporal separation: The bug was introduced by the interaction of two commits 19 days apart:

    • Oct 16, 2025: BuildKeysFromRecord was added
    • Nov 4, 2025: Alias support was added to BuildKeys

    Each commit in isolation worked correctly. The bug only manifested when both features interacted.

  4. No end-to-end test: The existing test TestBuildKeysFromRecord (lines 91-124 of pkg/identitymap/identitymap_test.go) only tests the basic functionality without alias metadata. It doesn't verify that BuildKeysFromRecord produces the same keys as BuildKeys for devices with aliases.

  5. Different code paths: The creation of keys (via BuildKeys) and the reconstruction of keys (via BuildKeysFromRecord) happen in different code paths. During normal operation, keys are created successfully. The reconstruction only happens when checking for stale keys, and its failure is not visible to the application - it simply results in stale keys not being deleted.

  6. Monitoring gap: The KV store metrics likely track total entries and growth rate, but wouldn't necessarily distinguish between legitimate growth (more devices) and bug-related growth (accumulating stale alias keys).

Recommended fix

The fix requires two changes:

  1. Store alias metadata in attributes: Update buildIdentityAttributes in pkg/registry/identity_publisher.go to store alias-related metadata fields in the record's attributes. This ensures the data is available for reconstruction.

  2. Reconstruct alias metadata: Update BuildKeysFromRecord in pkg/identitymap/identitymap.go to extract and reconstruct alias metadata fields from the record's attributes, similar to how it currently handles armis_device_id, integration_id, etc.

Here's a sketch of the fix for BuildKeysFromRecord:

func BuildKeysFromRecord(record *Record) []Key {
	if record == nil {
		return nil
	}

	update := &models.DeviceUpdate{
		DeviceID:  record.CanonicalDeviceID,
		Partition: record.Partition,
	}

	if record.Attributes != nil {
		if ip := strings.TrimSpace(record.Attributes["ip"]); ip != "" {
			update.IP = ip
		}
		if mac := strings.TrimSpace(record.Attributes["mac"]); mac != "" {
			macUpper := strings.ToUpper(mac)
			update.MAC = &macUpper
		}

		metaKeys := []string{"armis_device_id", "integration_id", "integration_type", "netbox_device_id"}
		for _, key := range metaKeys {
			if val := strings.TrimSpace(record.Attributes[key]); val != "" {
				if update.Metadata == nil {
					update.Metadata = make(map[string]string)
				}
				update.Metadata[key] = val
			}
		}

		// Reconstruct alias metadata  // <-- FIX 🟢
		aliasKeys := []string{"_alias_last_seen_service_id", "_alias_last_seen_ip"}
		for _, key := range aliasKeys {
			if val := strings.TrimSpace(record.Attributes[key]); val != "" {
				if update.Metadata == nil {
					update.Metadata = make(map[string]string)
				}
				update.Metadata[key] = val
			}
		}

		// Reconstruct service_alias:* and ip_alias:* fields  // <-- FIX 🟢
		for key, val := range record.Attributes {
			if strings.HasPrefix(key, "service_alias:") || strings.HasPrefix(key, "ip_alias:") {
				if update.Metadata == nil {
					update.Metadata = make(map[string]string)
				}
				update.Metadata[key] = val
			}
		}
	}

	return BuildKeys(update)
}

Note: This fix also requires updating buildIdentityAttributes to actually store these fields, which it currently doesn't do.

Imported from GitHub. Original GitHub issue: #2152 Original author: @mfreeman451 Original URL: https://github.com/carverauto/serviceradar/issues/2152 Original created: 2025-12-16T05:18:46Z --- # Summary - **Context**: The identity map system stores canonical device records in a KV store and maintains multiple lookup keys (by IP, MAC, device ID, etc.) that point to the same canonical record. When a device update occurs, the system creates new keys and removes stale keys that are no longer valid. - **Bug**: `BuildKeysFromRecord` fails to reconstruct alias-related identity keys (from `_alias_last_seen_service_id`, `_alias_last_seen_ip`, `service_alias:*`, and `ip_alias:*` metadata fields) that were originally created by `BuildKeys`. - **Actual vs. expected**: When the identity publisher retrieves an existing record to identify stale keys for deletion, it uses `BuildKeysFromRecord` to reconstruct the keys that should exist. Since `BuildKeysFromRecord` omits alias keys, these keys are never identified as stale and are never deleted, even when they should be. - **Impact**: Stale alias keys accumulate in the KV store indefinitely, causing the KV store to grow without bound. This undermines the purpose of the stale key deletion mechanism introduced in commit 3a5787ac ("stop KV from growing out of control"). # Code with bug In `pkg/identitymap/identitymap.go`, the `BuildKeysFromRecord` function only reconstructs a subset of metadata fields: ```go // BuildKeysFromRecord reconstructs the identity keys for a canonical record. func BuildKeysFromRecord(record *Record) []Key { if record == nil { return nil } update := &models.DeviceUpdate{ DeviceID: record.CanonicalDeviceID, Partition: record.Partition, } if record.Attributes != nil { if ip := strings.TrimSpace(record.Attributes["ip"]); ip != "" { update.IP = ip } if mac := strings.TrimSpace(record.Attributes["mac"]); mac != "" { macUpper := strings.ToUpper(mac) update.MAC = &macUpper } metaKeys := []string{"armis_device_id", "integration_id", "integration_type", "netbox_device_id"} for _, key := range metaKeys { if val := strings.TrimSpace(record.Attributes[key]); val != "" { if update.Metadata == nil { update.Metadata = make(map[string]string) } update.Metadata[key] = val // <-- BUG 🔴 Missing alias metadata fields } } } return BuildKeys(update) } ``` The `metaKeys` list on line 151 does not include any of the alias-related metadata fields that `BuildKeys` uses (lines 97-119): - `_alias_last_seen_service_id` - `_alias_last_seen_ip` - Keys with prefix `service_alias:` - Keys with prefix `ip_alias:` # Evidence ## Example Consider a device update with alias metadata: ```go update := &models.DeviceUpdate{ DeviceID: "tenant-a:host-device", IP: "10.0.0.5", Partition: "tenant-a", Metadata: map[string]string{ "_alias_last_seen_service_id": "serviceradar:agent:k8s-agent", "_alias_last_seen_ip": "10.0.0.8", "service_alias:serviceradar:poller:k8s": "2025-11-03T15:00:00Z", "ip_alias:10.0.0.9": "2025-11-03T15:00:00Z", }, } ``` When `BuildKeys(update)` is called, it creates 13 keys including: 1. `KindDeviceID: "tenant-a:host-device"` (the main device ID) 2. `KindDeviceID: "serviceradar:agent:k8s-agent"` (from `_alias_last_seen_service_id`) 3. `KindDeviceID: "serviceradar:poller:k8s"` (from `service_alias:serviceradar:poller:k8s`) 4. `KindIP: "10.0.0.5"` (main IP) 5. `KindIP: "10.0.0.8"` (from `_alias_last_seen_ip`) 6. `KindIP: "10.0.0.9"` (from `ip_alias:10.0.0.9`) 7. `KindPartitionIP: "tenant-a:10.0.0.5"` 8. `KindPartitionIP: "tenant-a:10.0.0.8"` 9. `KindPartitionIP: "tenant-a:10.0.0.9"` These keys are stored in the KV store pointing to the canonical record. However, when the record is later retrieved and `BuildKeysFromRecord` is called (in `identity_publisher.go:521`), only 3 keys are reconstructed: 1. `KindDeviceID: "tenant-a:host-device"` 2. `KindIP: "10.0.0.5"` 3. `KindPartitionIP: "tenant-a:10.0.0.5"` The 6 alias-related keys are missing from the reconstruction. ## Inconsistency within the codebase ### Reference code: BuildKeys in pkg/identitymap/identitymap.go (lines 97-119) ```go if aliasService := strings.TrimSpace(update.Metadata["_alias_last_seen_service_id"]); aliasService != "" { add(KindDeviceID, aliasService) } if aliasIP := strings.TrimSpace(update.Metadata["_alias_last_seen_ip"]); aliasIP != "" { add(KindIP, aliasIP) add(KindPartitionIP, partitionIPValue(update.Partition, aliasIP)) } for key := range update.Metadata { switch { case strings.HasPrefix(key, "service_alias:"): aliasID := strings.TrimSpace(strings.TrimPrefix(key, "service_alias:")) if aliasID != "" { add(KindDeviceID, aliasID) } case strings.HasPrefix(key, "ip_alias:"): aliasIP := strings.TrimSpace(strings.TrimPrefix(key, "ip_alias:")) if aliasIP != "" { add(KindIP, aliasIP) add(KindPartitionIP, partitionIPValue(update.Partition, aliasIP)) } } } ``` ### Current code: BuildKeysFromRecord in pkg/identitymap/identitymap.go (lines 131-163) ```go func BuildKeysFromRecord(record *Record) []Key { if record == nil { return nil } update := &models.DeviceUpdate{ DeviceID: record.CanonicalDeviceID, Partition: record.Partition, } if record.Attributes != nil { if ip := strings.TrimSpace(record.Attributes["ip"]); ip != "" { update.IP = ip } if mac := strings.TrimSpace(record.Attributes["mac"]); mac != "" { macUpper := strings.ToUpper(mac) update.MAC = &macUpper } metaKeys := []string{"armis_device_id", "integration_id", "integration_type", "netbox_device_id"} for _, key := range metaKeys { if val := strings.TrimSpace(record.Attributes[key]); val != "" { if update.Metadata == nil { update.Metadata = make(map[string]string) } update.Metadata[key] = val } } } return BuildKeys(update) } ``` ### Contradiction `BuildKeys` processes four categories of alias metadata fields to create identity keys: 1. `_alias_last_seen_service_id` → creates a `KindDeviceID` key 2. `_alias_last_seen_ip` → creates `KindIP` and `KindPartitionIP` keys 3. `service_alias:*` prefixed keys → creates `KindDeviceID` keys 4. `ip_alias:*` prefixed keys → creates `KindIP` and `KindPartitionIP` keys However, `BuildKeysFromRecord` only reconstructs metadata for `armis_device_id`, `integration_id`, `integration_type`, and `netbox_device_id`. None of the alias-related fields are included in the reconstruction, causing `BuildKeys` to produce a different set of keys when called from `BuildKeysFromRecord` versus when called directly with the original update. This violates the documented purpose of `BuildKeysFromRecord`: to "reconstruct the identity keys for a canonical record" (line 131). The function does not actually reconstruct all the keys that were originally created. ## Failing test ### Test script ```go package identitymap import ( "testing" "github.com/carverauto/serviceradar/pkg/models" "github.com/stretchr/testify/assert" ) // TestBuildKeysFromRecordIncludesAliasKeys verifies that BuildKeysFromRecord // reconstructs all keys that were originally created by BuildKeys, including // alias-related keys. // // This test currently fails, demonstrating a bug where alias keys are lost // during record reconstruction. func TestBuildKeysFromRecordIncludesAliasKeys(t *testing.T) { mac := "aa:bb:cc:dd:ee:ff" update := &models.DeviceUpdate{ DeviceID: "tenant-a:host-device", IP: "10.0.0.5", Partition: "tenant-a", MAC: &mac, Metadata: map[string]string{ "_alias_last_seen_service_id": "serviceradar:agent:k8s-agent", "_alias_last_seen_ip": "10.0.0.8", "service_alias:serviceradar:poller:k8s": "2025-11-03T15:00:00Z", "ip_alias:10.0.0.9": "2025-11-03T15:00:00Z", "armis_device_id": "armis-123", "integration_type": "netbox", "integration_id": "nb-42", "netbox_device_id": "123", }, } // Create a record as it would be stored (mimicking buildIdentityAttributes) record := &Record{ CanonicalDeviceID: update.DeviceID, Partition: update.Partition, MetadataHash: HashIdentityMetadata(update), Attributes: map[string]string{ "ip": update.IP, "mac": "AA:BB:CC:DD:EE:FF", "armis_device_id": update.Metadata["armis_device_id"], "integration_id": update.Metadata["integration_id"], "integration_type": update.Metadata["integration_type"], "netbox_device_id": update.Metadata["netbox_device_id"], // Note: alias metadata is NOT stored in attributes by buildIdentityAttributes }, } keysFromUpdate := BuildKeys(update) keysFromRecord := BuildKeysFromRecord(record) // This assertion will fail because BuildKeysFromRecord doesn't reconstruct // alias keys (service_alias:*, ip_alias:*, _alias_last_seen_service_id, _alias_last_seen_ip) assert.ElementsMatch(t, keysFromUpdate, keysFromRecord, "BuildKeysFromRecord should reconstruct all keys that BuildKeys originally created") // Specifically verify the alias keys are present aliasServiceKey := Key{Kind: KindDeviceID, Value: "serviceradar:agent:k8s-agent"} assert.Contains(t, keysFromRecord, aliasServiceKey, "Should include alias service ID from _alias_last_seen_service_id") aliasServiceKey2 := Key{Kind: KindDeviceID, Value: "serviceradar:poller:k8s"} assert.Contains(t, keysFromRecord, aliasServiceKey2, "Should include alias service ID from service_alias: prefix") aliasIPKey := Key{Kind: KindIP, Value: "10.0.0.8"} assert.Contains(t, keysFromRecord, aliasIPKey, "Should include alias IP from _alias_last_seen_ip") aliasIPKey2 := Key{Kind: KindIP, Value: "10.0.0.9"} assert.Contains(t, keysFromRecord, aliasIPKey2, "Should include alias IP from ip_alias: prefix") } ``` ### Test output ``` === RUN TestBuildKeysFromRecordIncludesAliasKeys alias_keys_bug_test.go:56: Error Trace: /home/user/serviceradar/pkg/identitymap/alias_keys_bug_test.go:56 Error: elements differ extra elements in list A: ([]interface {}) (len=6) { (identitymap.Key) { Kind: (identitymappb.IdentityKind) 1, Value: (string) (len=28) "serviceradar:agent:k8s-agent" }, (identitymap.Key) { Kind: (identitymappb.IdentityKind) 5, Value: (string) (len=8) "10.0.0.8" }, (identitymap.Key) { Kind: (identitymappb.IdentityKind) 6, Value: (string) (len=17) "tenant-a:10.0.0.8" }, (identitymap.Key) { Kind: (identitymappb.IdentityKind) 5, Value: (string) (len=8) "10.0.0.9" }, (identitymap.Key) { Kind: (identitymappb.IdentityKind) 6, Value: (string) (len=17) "tenant-a:10.0.0.9" }, (identitymap.Key) { Kind: (identitymappb.IdentityKind) 1, Value: (string) (len=23) "serviceradar:poller:k8s" } } listA: ([]identitymap.Key) (len=13) { (identitymap.Key) { Kind: (identitymappb.IdentityKind) 1, Value: (string) (len=20) "tenant-a:host-device" }, (identitymap.Key) { Kind: (identitymappb.IdentityKind) 5, Value: (string) (len=8) "10.0.0.5" }, (identitymap.Key) { Kind: (identitymappb.IdentityKind) 6, Value: (string) (len=17) "tenant-a:10.0.0.5" }, (identitymap.Key) { Kind: (identitymappb.IdentityKind) 2, Value: (string) (len=9) "armis-123" }, (identitymap.Key) { Kind: (identitymappb.IdentityKind) 3, Value: (string) (len=5) "nb-42" }, (identitymap.Key) { Kind: (identitymappb.IdentityKind) 3, Value: (string) (len=3) "123" }, (identitymap.Key) { Kind: (identitymappb.IdentityKind) 1, Value: (string) (len=28) "serviceradar:agent:k8s-agent" }, (identitymap.Key) { Kind: (identitymappb.IdentityKind) 5, Value: (string) (len=8) "10.0.0.8" }, (identitymap.Key) { Kind: (identitymappb.IdentityKind) 6, Value: (string) (len=17) "tenant-a:10.0.0.8" }, (identitymap.Key) { Kind: (identitymappb.IdentityKind) 5, Value: (string) (len=8) "10.0.0.9" }, (identitymap.Key) { Kind: (identitymappb.IdentityKind) 6, Value: (string) (len=17) "tenant-a:10.0.0.9" }, (identitymap.Key) { Kind: (identitymappb.IdentityKind) 1, Value: (string) (len=23) "serviceradar:poller:k8s" }, (identitymap.Key) { Kind: (identitymappb.IdentityKind) 4, Value: (string) (len=17) "AA:BB:CC:DD:EE:FF" } } listB: ([]identitymap.Key) (len=7) { (identitymap.Key) { Kind: (identitymappb.IdentityKind) 1, Value: (string) (len=20) "tenant-a:host-device" }, (identitymap.Key) { Kind: (identitymappb.IdentityKind) 5, Value: (string) (len=8) "10.0.0.5" }, (identitymap.Key) { Kind: (identitymappb.IdentityKind) 6, Value: (string) (len=17) "tenant-a:10.0.0.5" }, (identitymap.Key) { Kind: (identitymappb.IdentityKind) 2, Value: (string) (len=9) "armis-123" }, (identitymap.Key) { Kind: (identitymappb.IdentityKind) 3, Value: (string) (len=5) "nb-42" }, (identitymap.Key) { Kind: (identitymappb.IdentityKind) 3, Value: (string) (len=3) "123" }, (identitymap.Key) { Kind: (identitymappb.IdentityKind) 4, Value: (string) (len=17) "AA:BB:CC:DD:EE:FF" } } Test: TestBuildKeysFromRecordIncludesAliasKeys Messages: BuildKeysFromRecord should reconstruct all keys that BuildKeys originally created alias_keys_bug_test.go:61: Error Trace: /home/user/serviceradar/pkg/identitymap/alias_keys_bug_test.go:61 Error: []identitymap.Key{identitymap.Key{Kind:1, Value:"tenant-a:host-device"}, identitymap.Key{Kind:5, Value:"10.0.0.5"}, identitymap.Key{Kind:6, Value:"tenant-a:10.0.0.5"}, identitymap.Key{Kind:2, Value:"armis-123"}, identitymap.Key{Kind:3, Value:"nb-42"}, identitymap.Key{Kind:3, Value:"123"}, identitymap.Key{Kind:4, Value:"AA:BB:CC:DD:EE:FF"}} does not contain identitymap.Key{Kind:1, Value:"serviceradar:agent:k8s-agent"} Test: TestBuildKeysFromRecordIncludesAliasKeys Messages: Should include alias service ID from _alias_last_seen_service_id alias_keys_bug_test.go:65: Error Trace: /home/user/serviceradar/pkg/identitymap/alias_keys_bug_test.go:65 Error: []identitymap.Key{identitymap.Key{Kind:1, Value:"tenant-a:host-device"}, identitymap.Key{Kind:5, Value:"10.0.0.5"}, identitymap.Key{Kind:6, Value:"tenant-a:10.0.0.5"}, identitymap.Key{Kind:2, Value:"armis-123"}, identitymap.Key{Kind:3, Value:"nb-42"}, identitymap.Key{Kind:3, Value:"123"}, identitymap.Key{Kind:4, Value:"AA:BB:CC:DD:EE:FF"}} does not contain identitymap.Key{Kind:1, Value:"serviceradar:poller:k8s"} Test: TestBuildKeysFromRecordIncludesAliasKeys Messages: Should include alias service ID from service_alias: prefix alias_keys_bug_test.go:69: Error Trace: /home/user/serviceradar/pkg/identitymap/alias_keys_bug_test.go:69 Error: []identitymap.Key{identitymap.Key{Kind:1, Value:"tenant-a:host-device"}, identitymap.Key{Kind:5, Value:"10.0.0.5"}, identitymap.Key{Kind:6, Value:"tenant-a:10.0.0.5"}, identitymap.Key{Kind:2, Value:"armis-123"}, identitymap.Key{Kind:3, Value:"nb-42"}, identitymap.Key{Kind:3, Value:"123"}, identitymap.Key{Kind:4, Value:"AA:BB:CC:DD:EE:FF"}} does not contain identitymap.Key{Kind:5, Value:"10.0.0.8"} Test: TestBuildKeysFromRecordIncludesAliasKeys Messages: Should include alias IP from _alias_last_seen_ip alias_keys_bug_test.go:73: Error Trace: /home/user/serviceradar/pkg/identitymap/alias_keys_bug_test.go:73 Error: []identitymap.Key{identitymap.Key{Kind:1, Value:"tenant-a:host-device"}, identitymap.Key{Kind:5, Value:"10.0.0.5"}, identitymap.Key{Kind:6, Value:"tenant-a:10.0.0.5"}, identitymap.Key{Kind:2, Value:"armis-123"}, identitymap.Key{Kind:3, Value:"nb-42"}, identitymap.Key{Kind:3, Value:"123"}, identitymap.Key{Kind:4, Value:"AA:BB:CC:DD:EE:FF"}} does not contain identitymap.Key{Kind:5, Value:"10.0.0.9"} Test: TestBuildKeysFromRecordIncludesAliasKeys Messages: Should include alias IP from ip_alias: prefix --- FAIL: TestBuildKeysFromRecordIncludesAliasKeys (0.00s) FAIL FAIL github.com/carverauto/serviceradar/pkg/identitymap 0.006s FAIL ``` The test clearly shows that `BuildKeysFromRecord` produces only 7 keys while `BuildKeys` produces 13 keys for the same device. The 6 missing keys are all alias-related keys. # Full context The identity map system is used to maintain a canonical mapping of device identities across the ServiceRadar platform. When a device is discovered or updated, multiple identity keys are created (by IP address, MAC address, device ID, integration IDs, etc.) that all point to the same canonical device record stored in a NATS JetStream KV store. The `identityPublisher` in `pkg/registry/identity_publisher.go` is responsible for publishing these mappings to the KV store. When publishing an update for a device: 1. It calls `BuildKeys(update)` to generate all identity keys for the new update (lines 216-222) 2. It retrieves the existing canonical record from the KV store to find what keys currently exist (line 206, which calls `existingIdentitySnapshot`) 3. Inside `existingIdentitySnapshot`, it calls `BuildKeysFromRecord(record)` to reconstruct what keys should exist based on the stored record (line 521) 4. It compares the new key set with the reconstructed old key set to identify stale keys (line 225) 5. It deletes the stale keys from the KV store (lines 226-228) This stale key deletion mechanism was explicitly added in commit 3a5787ac on Oct 16, 2025 to "stop KV from growing out of control." However, on Nov 4, 2025 (commit 5223ac8c), alias support was added to `BuildKeys` to track device aliases (when a device is known by multiple service IDs or IP addresses). This allows the system to create additional lookup keys for aliased identities. The commit added support for: - `_alias_last_seen_service_id`: The last service ID this device was seen as - `_alias_last_seen_ip`: The last IP address this device was seen at - `service_alias:*`: Historical service IDs this device has been known as - `ip_alias:*`: Historical IP addresses this device has been associated with The problem is that `BuildKeysFromRecord` was not updated to handle these alias fields. Additionally, the `buildIdentityAttributes` function in `pkg/registry/identity_publisher.go` (lines 408-450) does not store alias metadata in the record's attributes, so even if `BuildKeysFromRecord` tried to reconstruct them, the data wouldn't be available. This means: 1. When a device with aliases is published, alias keys are created in the KV store 2. When the same device is later updated (even if aliases change or are removed), the old alias keys are never identified as stale 3. These orphaned alias keys remain in the KV store indefinitely, accumulating over time 4. The KV store grows without bound, exactly the problem that commit 3a5787ac was meant to prevent The device alias feature is actively used in the codebase (see `pkg/core/alias_events.go`, `pkg/devicealias/alias.go`) to track when devices are seen under different identities, which is an important feature for device tracking in complex networks. # Why has this bug gone undetected? This bug has gone undetected for several reasons: 1. **Silent failure**: The bug doesn't cause any errors or exceptions. The identity publisher successfully creates the alias keys and successfully publishes updates. The only symptom is that stale keys are not deleted, which is not immediately observable. 2. **Gradual accumulation**: The effect of the bug is a gradual accumulation of stale keys over time. In a test environment or during initial deployment, the KV store might not grow large enough to be noticed. Only in production with sustained use would the KV store growth become apparent. 3. **Temporal separation**: The bug was introduced by the interaction of two commits 19 days apart: - Oct 16, 2025: `BuildKeysFromRecord` was added - Nov 4, 2025: Alias support was added to `BuildKeys` Each commit in isolation worked correctly. The bug only manifested when both features interacted. 4. **No end-to-end test**: The existing test `TestBuildKeysFromRecord` (lines 91-124 of `pkg/identitymap/identitymap_test.go`) only tests the basic functionality without alias metadata. It doesn't verify that `BuildKeysFromRecord` produces the same keys as `BuildKeys` for devices with aliases. 5. **Different code paths**: The creation of keys (via `BuildKeys`) and the reconstruction of keys (via `BuildKeysFromRecord`) happen in different code paths. During normal operation, keys are created successfully. The reconstruction only happens when checking for stale keys, and its failure is not visible to the application - it simply results in stale keys not being deleted. 6. **Monitoring gap**: The KV store metrics likely track total entries and growth rate, but wouldn't necessarily distinguish between legitimate growth (more devices) and bug-related growth (accumulating stale alias keys). # Recommended fix The fix requires two changes: 1. **Store alias metadata in attributes**: Update `buildIdentityAttributes` in `pkg/registry/identity_publisher.go` to store alias-related metadata fields in the record's attributes. This ensures the data is available for reconstruction. 2. **Reconstruct alias metadata**: Update `BuildKeysFromRecord` in `pkg/identitymap/identitymap.go` to extract and reconstruct alias metadata fields from the record's attributes, similar to how it currently handles `armis_device_id`, `integration_id`, etc. Here's a sketch of the fix for `BuildKeysFromRecord`: ```go func BuildKeysFromRecord(record *Record) []Key { if record == nil { return nil } update := &models.DeviceUpdate{ DeviceID: record.CanonicalDeviceID, Partition: record.Partition, } if record.Attributes != nil { if ip := strings.TrimSpace(record.Attributes["ip"]); ip != "" { update.IP = ip } if mac := strings.TrimSpace(record.Attributes["mac"]); mac != "" { macUpper := strings.ToUpper(mac) update.MAC = &macUpper } metaKeys := []string{"armis_device_id", "integration_id", "integration_type", "netbox_device_id"} for _, key := range metaKeys { if val := strings.TrimSpace(record.Attributes[key]); val != "" { if update.Metadata == nil { update.Metadata = make(map[string]string) } update.Metadata[key] = val } } // Reconstruct alias metadata // <-- FIX 🟢 aliasKeys := []string{"_alias_last_seen_service_id", "_alias_last_seen_ip"} for _, key := range aliasKeys { if val := strings.TrimSpace(record.Attributes[key]); val != "" { if update.Metadata == nil { update.Metadata = make(map[string]string) } update.Metadata[key] = val } } // Reconstruct service_alias:* and ip_alias:* fields // <-- FIX 🟢 for key, val := range record.Attributes { if strings.HasPrefix(key, "service_alias:") || strings.HasPrefix(key, "ip_alias:") { if update.Metadata == nil { update.Metadata = make(map[string]string) } update.Metadata[key] = val } } } return BuildKeys(update) } ``` Note: This fix also requires updating `buildIdentityAttributes` to actually store these fields, which it currently doesn't do.
Author
Owner

Imported GitHub comment.

Original author: @mfreeman451
Original URL: https://github.com/carverauto/serviceradar/issues/2152#issuecomment-3663247422
Original created: 2025-12-17T01:49:02Z


closing as completed

Imported GitHub comment. Original author: @mfreeman451 Original URL: https://github.com/carverauto/serviceradar/issues/2152#issuecomment-3663247422 Original created: 2025-12-17T01:49:02Z --- closing as completed
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
carverauto/serviceradar#690
No description provided.