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:
parent
0d0a7320e6
commit
f4ec95d027
@ -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)
|
||||
|
@ -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))
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user