diff --git a/cmd/containerd-shim-runc-v2/manager/manager_linux.go b/cmd/containerd-shim-runc-v2/manager/manager_linux.go index 4e2f1c1195..ee7e281308 100644 --- a/cmd/containerd-shim-runc-v2/manager/manager_linux.go +++ b/cmd/containerd-shim-runc-v2/manager/manager_linux.go @@ -24,10 +24,12 @@ import ( "fmt" "io" "net" + "net/url" "os" "os/exec" "path/filepath" goruntime "runtime" + "strings" "syscall" "time" @@ -105,7 +107,20 @@ func newCommand(ctx context.Context, id, containerdAddress, containerdTTRPCAddre cmd := exec.Command(self, args...) cmd.Dir = cwd cmd.Env = append(os.Environ(), "GOMAXPROCS=4") - cmd.Env = append(cmd.Env, "OTEL_SERVICE_NAME=containerd-shim-"+id) + // Preserve inherited resource attributes, but override the daemon's identity. + // The OpenTelemetry SDK uses the last value for duplicate resource keys. + var attrs []string + if inherited := strings.TrimSpace(os.Getenv("OTEL_RESOURCE_ATTRIBUTES")); inherited != "" { + attrs = append(attrs, inherited) + } + attrs = append(attrs, + "service.instance.id="+url.PathEscape(id), + "service.version="+url.PathEscape(version.Version), + ) + cmd.Env = append(cmd.Env, + "OTEL_SERVICE_NAME=containerd-shim-runc-v2", + "OTEL_RESOURCE_ATTRIBUTES="+strings.Join(attrs, ","), + ) cmd.SysProcAttr = &syscall.SysProcAttr{ Setpgid: true, diff --git a/cmd/containerd-shim-runc-v2/manager/manager_linux_test.go b/cmd/containerd-shim-runc-v2/manager/manager_linux_test.go new file mode 100644 index 0000000000..49e07ad301 --- /dev/null +++ b/cmd/containerd-shim-runc-v2/manager/manager_linux_test.go @@ -0,0 +1,84 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package manager + +import ( + "context" + "strings" + "testing" + + "github.com/containerd/containerd/v2/pkg/namespaces" + "github.com/containerd/containerd/v2/version" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/sdk/resource" +) + +func TestNewCommandTracingResource(t *testing.T) { + for _, tc := range []struct { + name string + inherited string + attributes map[string]string + }{ + {name: "empty"}, + {name: "whitespace", inherited: " "}, + { + name: "inherited", + inherited: "service.name=containerd,service.instance.id=daemon,service.version=old,service.namespace=runtime,host.name=node,custom.value=a%2Cb%3Dc%25", + attributes: map[string]string{ + "service.namespace": "runtime", + "host.name": "node", + "custom.value": "a,b=c%", + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Setenv("OTEL_SERVICE_NAME", "containerd") + t.Setenv("OTEL_RESOURCE_ATTRIBUTES", tc.inherited) + ctx := namespaces.WithNamespace(context.Background(), "test-ns") + for _, id := range []string{"test-shim", "another-shim"} { + cmd, err := newCommand(ctx, id, "/containerd.sock", "/containerd.sock.ttrpc", false) + require.NoError(t, err) + // Environ applies exec.Cmd's duplicate-variable handling. Read the + // resulting resource with the SDK without using its cached default. + for _, entry := range cmd.Environ() { + key, value, _ := strings.Cut(entry, "=") + if key == "OTEL_SERVICE_NAME" || key == "OTEL_RESOURCE_ATTRIBUTES" { + t.Setenv(key, value) + } + } + res, err := resource.New(ctx, resource.WithFromEnv()) + require.NoError(t, err) + attrs := res.Set() + for key, expected := range map[string]string{ + "service.name": "containerd-shim-runc-v2", + "service.instance.id": id, + "service.version": version.Version, + } { + value, ok := attrs.Value(attribute.Key(key)) + require.True(t, ok, key) + require.Equal(t, expected, value.AsString(), key) + } + for key, expected := range tc.attributes { + value, ok := attrs.Value(attribute.Key(key)) + require.True(t, ok, key) + require.Equal(t, expected, value.AsString(), key) + } + } + }) + } +}