Skip to content

Commit

Permalink
Add container id support to Resource (open-telemetry#2418)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
3 people committed Mar 5, 2022
1 parent 8386f01 commit 7632087
Show file tree
Hide file tree
Showing 7 changed files with 359 additions and 2 deletions.
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)

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

0 comments on commit 7632087

Please sign in to comment.