Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add container id support to Resource #2418

Merged
merged 16 commits into from Mar 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -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)
Expand Down
13 changes: 13 additions & 0 deletions sdk/resource/config.go
Expand Up @@ -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 changes: 100 additions & 0 deletions sdk/resource/container.go
@@ -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 changes: 169 additions & 0 deletions sdk/resource/container_test.go
@@ -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)
})
}
}
2 changes: 2 additions & 0 deletions sdk/resource/export_test.go
Expand Up @@ -23,6 +23,8 @@ var (
SetUserProviders = setUserProviders
SetDefaultOSDescriptionProvider = setDefaultOSDescriptionProvider
SetOSDescriptionProvider = setOSDescriptionProvider
SetDefaultContainerProviders = setDefaultContainerProviders
SetContainerProviders = setContainerProviders
)

var (
Expand Down
5 changes: 3 additions & 2 deletions sdk/resource/process_test.go
Expand Up @@ -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)
Aneurysm9 marked this conversation as resolved.
Show resolved Hide resolved

restoreAttributesProviders()
}
Expand Down
71 changes: 71 additions & 0 deletions sdk/resource/resource_test.go
Expand Up @@ -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))
}