1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2025-01-18 03:22:12 +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:
Sam Xie 2022-03-04 01:13:31 +08:00 committed by GitHub
parent 0d0a7320e6
commit f4ec95d027
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 359 additions and 2 deletions

View File

@ -26,6 +26,7 @@ This update is a breaking change of the unstable Metrics API. Code instrumented
If the provided environment variables are invalid (negative), the default values would be used.
- Rename the `gc` runtime name to `go` (#2560)
- Add container id support to Resource. (#2418)
- Add span attribute value length limit.
The new `AttributeValueLengthLimit` field is added to the `"go.opentelemetry.io/otel/sdk/trace".SpanLimits` type to configure this limit for a `TracerProvider`.
The default limit for this resource is "unlimited". (#2637)

View File

@ -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
View 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]
}

View 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)
})
}
}

View File

@ -23,6 +23,8 @@ var (
SetUserProviders = setUserProviders
SetDefaultOSDescriptionProvider = setDefaultOSDescriptionProvider
SetOSDescriptionProvider = setOSDescriptionProvider
SetDefaultContainerProviders = setDefaultContainerProviders
SetContainerProviders = setContainerProviders
)
var (

View File

@ -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()
}

View File

@ -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))
}