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:
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.
|
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)
|
||||||
|
@ -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
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
|
SetUserProviders = setUserProviders
|
||||||
SetDefaultOSDescriptionProvider = setDefaultOSDescriptionProvider
|
SetDefaultOSDescriptionProvider = setDefaultOSDescriptionProvider
|
||||||
SetOSDescriptionProvider = setOSDescriptionProvider
|
SetOSDescriptionProvider = setOSDescriptionProvider
|
||||||
|
SetDefaultContainerProviders = setDefaultContainerProviders
|
||||||
|
SetContainerProviders = setContainerProviders
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user