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 13 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 @@ -23,6 +23,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)
- Log the Exporters configuration in the TracerProviders message. (#2578)
- Add container id support to Resource. (#2418)

### Changed

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{})
}
101 changes: 101 additions & 0 deletions sdk/resource/container.go
@@ -0,0 +1,101 @@
// 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
)

type cgroupContainerIDDetector struct{}

const cgroupPath = "/proc/self/cgroup"

var cgroupContainerIDRe = regexp.MustCompile(`^.*/(?:.*-)?([0-9a-f]+)(?:\.|\s*$)`)
XSAM marked this conversation as resolved.
Show resolved Hide resolved

// 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 matches[1]
}
return ""
XSAM marked this conversation as resolved.
Show resolved Hide resolved
}
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
72 changes: 72 additions & 0 deletions sdk/resource/resource_test.go
Expand Up @@ -649,3 +649,75 @@ 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)

XSAM marked this conversation as resolved.
Show resolved Hide resolved
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))
}