You've already forked opentelemetry-go
							
							
				mirror of
				https://github.com/open-telemetry/opentelemetry-go.git
				synced 2025-10-31 00:07:40 +02:00 
			
		
		
		
	Add container id support to Resource (#2418)
* Add container id support to Resource * Fix wrong test case name * Add WithContainer option * Update CHANGELOG * Fix comments * Update CHANGELOG * Use regex to find container id * Add tests for reading cgroup file * Update sdk/resource/container.go Co-authored-by: Chester Cheung <cheung.zhy.csu@gmail.com> * Update format Co-authored-by: Chester Cheung <cheung.zhy.csu@gmail.com> Co-authored-by: Anthony Mirabella <a9@aneurysm9.com>
This commit is contained in:
		| @@ -171,3 +171,16 @@ func WithProcessRuntimeVersion() Option { | ||||
| func WithProcessRuntimeDescription() Option { | ||||
| 	return WithDetectors(processRuntimeDescriptionDetector{}) | ||||
| } | ||||
|  | ||||
| // WithContainer adds all the Container attributes to the configured Resource. | ||||
| // See individual WithContainer* functions to configure specific attributes. | ||||
| func WithContainer() Option { | ||||
| 	return WithDetectors( | ||||
| 		cgroupContainerIDDetector{}, | ||||
| 	) | ||||
| } | ||||
|  | ||||
| // WithContainerID adds an attribute with the id of the container to the configured Resource. | ||||
| func WithContainerID() Option { | ||||
| 	return WithDetectors(cgroupContainerIDDetector{}) | ||||
| } | ||||
|   | ||||
							
								
								
									
										100
									
								
								sdk/resource/container.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								sdk/resource/container.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,100 @@ | ||||
| // Copyright The OpenTelemetry 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 resource // import "go.opentelemetry.io/otel/sdk/resource" | ||||
|  | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"io" | ||||
| 	"os" | ||||
| 	"regexp" | ||||
|  | ||||
| 	semconv "go.opentelemetry.io/otel/semconv/v1.7.0" | ||||
| ) | ||||
|  | ||||
| type containerIDProvider func() (string, error) | ||||
|  | ||||
| var ( | ||||
| 	containerID         containerIDProvider = getContainerIDFromCGroup | ||||
| 	cgroupContainerIDRe                     = regexp.MustCompile(`^.*/(?:.*-)?([0-9a-f]+)(?:\.|\s*$)`) | ||||
| ) | ||||
|  | ||||
| type cgroupContainerIDDetector struct{} | ||||
|  | ||||
| const cgroupPath = "/proc/self/cgroup" | ||||
|  | ||||
| // Detect returns a *Resource that describes the id of the container. | ||||
| // If no container id found, an empty resource will be returned. | ||||
| func (cgroupContainerIDDetector) Detect(ctx context.Context) (*Resource, error) { | ||||
| 	containerID, err := containerID() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if containerID == "" { | ||||
| 		return Empty(), nil | ||||
| 	} | ||||
| 	return NewWithAttributes(semconv.SchemaURL, semconv.ContainerIDKey.String(containerID)), nil | ||||
| } | ||||
|  | ||||
| var ( | ||||
| 	defaultOSStat = os.Stat | ||||
| 	osStat        = defaultOSStat | ||||
|  | ||||
| 	defaultOSOpen = func(name string) (io.ReadCloser, error) { | ||||
| 		return os.Open(name) | ||||
| 	} | ||||
| 	osOpen = defaultOSOpen | ||||
| ) | ||||
|  | ||||
| // getContainerIDFromCGroup returns the id of the container from the cgroup file. | ||||
| // If no container id found, an empty string will be returned. | ||||
| func getContainerIDFromCGroup() (string, error) { | ||||
| 	if _, err := osStat(cgroupPath); errors.Is(err, os.ErrNotExist) { | ||||
| 		// File does not exist, skip | ||||
| 		return "", nil | ||||
| 	} | ||||
|  | ||||
| 	file, err := osOpen(cgroupPath) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	defer file.Close() | ||||
|  | ||||
| 	return getContainerIDFromReader(file), nil | ||||
| } | ||||
|  | ||||
| // getContainerIDFromReader returns the id of the container from reader. | ||||
| func getContainerIDFromReader(reader io.Reader) string { | ||||
| 	scanner := bufio.NewScanner(reader) | ||||
| 	for scanner.Scan() { | ||||
| 		line := scanner.Text() | ||||
|  | ||||
| 		if id := getContainerIDFromLine(line); id != "" { | ||||
| 			return id | ||||
| 		} | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| // getContainerIDFromLine returns the id of the container from one string line. | ||||
| func getContainerIDFromLine(line string) string { | ||||
| 	matches := cgroupContainerIDRe.FindStringSubmatch(line) | ||||
| 	if len(matches) <= 1 { | ||||
| 		return "" | ||||
| 	} | ||||
| 	return matches[1] | ||||
| } | ||||
							
								
								
									
										169
									
								
								sdk/resource/container_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								sdk/resource/container_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,169 @@ | ||||
| // Copyright The OpenTelemetry 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 resource | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"io" | ||||
| 	"os" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| func setDefaultContainerProviders() { | ||||
| 	setContainerProviders( | ||||
| 		getContainerIDFromCGroup, | ||||
| 	) | ||||
| } | ||||
|  | ||||
| func setContainerProviders( | ||||
| 	idProvider containerIDProvider, | ||||
| ) { | ||||
| 	containerID = idProvider | ||||
| } | ||||
|  | ||||
| func TestGetContainerIDFromLine(t *testing.T) { | ||||
| 	testCases := []struct { | ||||
| 		name                string | ||||
| 		line                string | ||||
| 		expectedContainerID string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:                "with suffix", | ||||
| 			line:                "13:name=systemd:/podruntime/docker/kubepods/ac679f8a8319c8cf7d38e1adf263bc08d23.aaaa", | ||||
| 			expectedContainerID: "ac679f8a8319c8cf7d38e1adf263bc08d23", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                "with prefix and suffix", | ||||
| 			line:                "13:name=systemd:/podruntime/docker/kubepods/crio-dc679f8a8319c8cf7d38e1adf263bc08d23.stuff", | ||||
| 			expectedContainerID: "dc679f8a8319c8cf7d38e1adf263bc08d23", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                "no prefix and suffix", | ||||
| 			line:                "13:name=systemd:/pod/d86d75589bf6cc254f3e2cc29debdf85dde404998aa128997a819ff991827356", | ||||
| 			expectedContainerID: "d86d75589bf6cc254f3e2cc29debdf85dde404998aa128997a819ff991827356", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                "with space", | ||||
| 			line:                " 13:name=systemd:/pod/d86d75589bf6cc254f3e2cc29debdf85dde404998aa128997a819ff991827356 ", | ||||
| 			expectedContainerID: "d86d75589bf6cc254f3e2cc29debdf85dde404998aa128997a819ff991827356", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "invalid hex string", | ||||
| 			line: "13:name=systemd:/podruntime/docker/kubepods/ac679f8a8319c8cf7d38e1adf263bc08d23zzzz", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "no container id - 1", | ||||
| 			line: "pids: /", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "no container id - 2", | ||||
| 			line: "pids: ", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tc := range testCases { | ||||
| 		t.Run(tc.name, func(t *testing.T) { | ||||
| 			containerID := getContainerIDFromLine(tc.line) | ||||
| 			assert.Equal(t, tc.expectedContainerID, containerID) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestGetContainerIDFromReader(t *testing.T) { | ||||
| 	testCases := []struct { | ||||
| 		name                string | ||||
| 		reader              io.Reader | ||||
| 		expectedContainerID string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "multiple lines", | ||||
| 			reader: strings.NewReader(`// | ||||
| 1:name=systemd:/podruntime/docker/kubepods/docker-dc579f8a8319c8cf7d38e1adf263bc08d23 | ||||
| 1:name=systemd:/podruntime/docker/kubepods/docker-dc579f8a8319c8cf7d38e1adf263bc08d24 | ||||
| `), | ||||
| 			expectedContainerID: "dc579f8a8319c8cf7d38e1adf263bc08d23", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "no container id", | ||||
| 			reader: strings.NewReader(`// | ||||
| 1:name=systemd:/podruntime/docker | ||||
| `), | ||||
| 			expectedContainerID: "", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tc := range testCases { | ||||
| 		t.Run(tc.name, func(t *testing.T) { | ||||
| 			containerID := getContainerIDFromReader(tc.reader) | ||||
| 			assert.Equal(t, tc.expectedContainerID, containerID) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestGetContainerIDFromCGroup(t *testing.T) { | ||||
| 	t.Cleanup(func() { | ||||
| 		osStat = defaultOSStat | ||||
| 		osOpen = defaultOSOpen | ||||
| 	}) | ||||
|  | ||||
| 	testCases := []struct { | ||||
| 		name                string | ||||
| 		cgroupFileNotExist  bool | ||||
| 		openFileError       error | ||||
| 		content             string | ||||
| 		expectedContainerID string | ||||
| 		expectedError       bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:               "the cgroup file does not exist", | ||||
| 			cgroupFileNotExist: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:          "error when opening cgroup file", | ||||
| 			openFileError: errors.New("test"), | ||||
| 			expectedError: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                "cgroup file", | ||||
| 			content:             "1:name=systemd:/podruntime/docker/kubepods/docker-dc579f8a8319c8cf7d38e1adf263bc08d23", | ||||
| 			expectedContainerID: "dc579f8a8319c8cf7d38e1adf263bc08d23", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tc := range testCases { | ||||
| 		t.Run(tc.name, func(t *testing.T) { | ||||
| 			osStat = func(name string) (os.FileInfo, error) { | ||||
| 				if tc.cgroupFileNotExist { | ||||
| 					return nil, os.ErrNotExist | ||||
| 				} | ||||
| 				return nil, nil | ||||
| 			} | ||||
|  | ||||
| 			osOpen = func(name string) (io.ReadCloser, error) { | ||||
| 				if tc.openFileError != nil { | ||||
| 					return nil, tc.openFileError | ||||
| 				} | ||||
| 				return io.NopCloser(strings.NewReader(tc.content)), nil | ||||
| 			} | ||||
|  | ||||
| 			containerID, err := getContainerIDFromCGroup() | ||||
| 			assert.Equal(t, tc.expectedError, err != nil) | ||||
| 			assert.Equal(t, tc.expectedContainerID, containerID) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| @@ -23,6 +23,8 @@ var ( | ||||
| 	SetUserProviders                = setUserProviders | ||||
| 	SetDefaultOSDescriptionProvider = setDefaultOSDescriptionProvider | ||||
| 	SetOSDescriptionProvider        = setOSDescriptionProvider | ||||
| 	SetDefaultContainerProviders    = setDefaultContainerProviders | ||||
| 	SetContainerProviders           = setContainerProviders | ||||
| ) | ||||
|  | ||||
| var ( | ||||
|   | ||||
| @@ -102,13 +102,14 @@ func restoreAttributesProviders() { | ||||
| 	resource.SetDefaultRuntimeProviders() | ||||
| 	resource.SetDefaultUserProviders() | ||||
| 	resource.SetDefaultOSDescriptionProvider() | ||||
| 	resource.SetDefaultContainerProviders() | ||||
| } | ||||
|  | ||||
| func TestWithProcessFuncsErrors(t *testing.T) { | ||||
| 	mockProcessAttributesProvidersWithErrors() | ||||
|  | ||||
| 	t.Run("WithPID", testWithProcessExecutablePathError) | ||||
| 	t.Run("WithExecutableName", testWithProcessOwnerError) | ||||
| 	t.Run("WithExecutablePath", testWithProcessExecutablePathError) | ||||
| 	t.Run("WithOwner", testWithProcessOwnerError) | ||||
|  | ||||
| 	restoreAttributesProviders() | ||||
| } | ||||
|   | ||||
| @@ -649,3 +649,74 @@ func hostname() string { | ||||
| 	} | ||||
| 	return hn | ||||
| } | ||||
|  | ||||
| func TestWithContainerID(t *testing.T) { | ||||
| 	t.Cleanup(restoreAttributesProviders) | ||||
|  | ||||
| 	fakeContainerID := "fake-container-id" | ||||
|  | ||||
| 	testCases := []struct { | ||||
| 		name                string | ||||
| 		containerIDProvider func() (string, error) | ||||
| 		expectedResource    map[string]string | ||||
| 		expectedErr         bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "get container id", | ||||
| 			containerIDProvider: func() (string, error) { | ||||
| 				return fakeContainerID, nil | ||||
| 			}, | ||||
| 			expectedResource: map[string]string{ | ||||
| 				string(semconv.ContainerIDKey): fakeContainerID, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "no container id found", | ||||
| 			containerIDProvider: func() (string, error) { | ||||
| 				return "", nil | ||||
| 			}, | ||||
| 			expectedResource: map[string]string{}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "error", | ||||
| 			containerIDProvider: func() (string, error) { | ||||
| 				return "", fmt.Errorf("unable to get container id") | ||||
| 			}, | ||||
| 			expectedResource: map[string]string{}, | ||||
| 			expectedErr:      true, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tc := range testCases { | ||||
| 		t.Run(tc.name, func(t *testing.T) { | ||||
| 			resource.SetContainerProviders(tc.containerIDProvider) | ||||
|  | ||||
| 			res, err := resource.New(context.Background(), | ||||
| 				resource.WithContainerID(), | ||||
| 			) | ||||
|  | ||||
| 			if tc.expectedErr { | ||||
| 				assert.Error(t, err) | ||||
| 			} | ||||
| 			assert.Equal(t, tc.expectedResource, toMap(res)) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestWithContainer(t *testing.T) { | ||||
| 	t.Cleanup(restoreAttributesProviders) | ||||
|  | ||||
| 	fakeContainerID := "fake-container-id" | ||||
| 	resource.SetContainerProviders(func() (string, error) { | ||||
| 		return fakeContainerID, nil | ||||
| 	}) | ||||
|  | ||||
| 	res, err := resource.New(context.Background(), | ||||
| 		resource.WithContainer(), | ||||
| 	) | ||||
|  | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, map[string]string{ | ||||
| 		string(semconv.ContainerIDKey): fakeContainerID, | ||||
| 	}, toMap(res)) | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user