1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2025-01-20 03:30:02 +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. If the provided environment variables are invalid (negative), the default values would be used.
- Rename the `gc` runtime name to `go` (#2560) - Rename the `gc` runtime name to `go` (#2560)
- Add container id support to Resource. (#2418)
- Add span attribute value length limit. - 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 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) The default limit for this resource is "unlimited". (#2637)

View File

@ -171,3 +171,16 @@ func WithProcessRuntimeVersion() Option {
func WithProcessRuntimeDescription() Option { func WithProcessRuntimeDescription() Option {
return WithDetectors(processRuntimeDescriptionDetector{}) 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 SetUserProviders = setUserProviders
SetDefaultOSDescriptionProvider = setDefaultOSDescriptionProvider SetDefaultOSDescriptionProvider = setDefaultOSDescriptionProvider
SetOSDescriptionProvider = setOSDescriptionProvider SetOSDescriptionProvider = setOSDescriptionProvider
SetDefaultContainerProviders = setDefaultContainerProviders
SetContainerProviders = setContainerProviders
) )
var ( var (

View File

@ -102,13 +102,14 @@ func restoreAttributesProviders() {
resource.SetDefaultRuntimeProviders() resource.SetDefaultRuntimeProviders()
resource.SetDefaultUserProviders() resource.SetDefaultUserProviders()
resource.SetDefaultOSDescriptionProvider() resource.SetDefaultOSDescriptionProvider()
resource.SetDefaultContainerProviders()
} }
func TestWithProcessFuncsErrors(t *testing.T) { func TestWithProcessFuncsErrors(t *testing.T) {
mockProcessAttributesProvidersWithErrors() mockProcessAttributesProvidersWithErrors()
t.Run("WithPID", testWithProcessExecutablePathError) t.Run("WithExecutablePath", testWithProcessExecutablePathError)
t.Run("WithExecutableName", testWithProcessOwnerError) t.Run("WithOwner", testWithProcessOwnerError)
restoreAttributesProviders() restoreAttributesProviders()
} }

View File

@ -649,3 +649,74 @@ func hostname() string {
} }
return hn 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))
}