diff --git a/apps/monitoring/containers/config.go b/apps/monitoring/containers/config.go index a974bbe3f3..ef26b8001f 100644 --- a/apps/monitoring/containers/config.go +++ b/apps/monitoring/containers/config.go @@ -51,11 +51,18 @@ func ShouldMonitorContainer(containerName string) bool { return true } +// GetServiceName returns the deduplication key of a container: its name +// without the swarm task suffix (myapp.1.abc123 → myapp), so replicas of the +// same swarm service are stored once per tick. Names without a task suffix +// (docker compose containers) are already unique per container and are kept +// as-is. Splitting on "-" here used to conflate distinct services sharing a +// name prefix (app-x-mysql and app-x-redis both mapped to "app-x"), so only +// the first of them in `docker stats` output was stored each tick — even +// though metrics are stored and queried by full container_name. func GetServiceName(containerName string) string { name := strings.TrimPrefix(containerName, "/") - parts := strings.Split(name, "-") - if len(parts) > 1 { - return strings.Join(parts[:len(parts)-1], "-") + if dot := strings.Index(name, "."); dot != -1 { + return name[:dot] } return name } diff --git a/apps/monitoring/containers/config_test.go b/apps/monitoring/containers/config_test.go new file mode 100644 index 0000000000..89df80a5bf --- /dev/null +++ b/apps/monitoring/containers/config_test.go @@ -0,0 +1,27 @@ +package containers + +import "testing" + +func TestGetServiceName(t *testing.T) { + cases := []struct { + name string + in string + want string + }{ + {"swarm replica 1", "myapp-3fa2bc.1.zxc4vplq8m0e", "myapp-3fa2bc"}, + {"swarm replica 2 collapses to the same service", "myapp-3fa2bc.2.a1b2c3d4e5f6", "myapp-3fa2bc"}, + {"leading slash is stripped", "/myapp-3fa2bc.1.zxc4vplq8m0e", "myapp-3fa2bc"}, + {"compose container_name stays distinct", "app-1425-mysql", "app-1425-mysql"}, + {"sibling compose service must not collapse with the previous one", "app-1425-redis", "app-1425-redis"}, + {"compose default naming stays distinct", "project-web-1", "project-web-1"}, + {"name without separators", "single", "single"}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := GetServiceName(tc.in); got != tc.want { + t.Errorf("GetServiceName(%q) = %q, want %q", tc.in, got, tc.want) + } + }) + } +}