Skip to content

Commit f4ec95d

Browse files
XSAMhanyuancheungAneurysm9
authored
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 <[email protected]> * Update format Co-authored-by: Chester Cheung <[email protected]> Co-authored-by: Anthony Mirabella <[email protected]>
1 parent 0d0a732 commit f4ec95d

File tree

7 files changed

+359
-2
lines changed

7 files changed

+359
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ This update is a breaking change of the unstable Metrics API. Code instrumented
2626

2727
If the provided environment variables are invalid (negative), the default values would be used.
2828
- Rename the `gc` runtime name to `go` (#2560)
29+
- Add container id support to Resource. (#2418)
2930
- Add span attribute value length limit.
3031
The new `AttributeValueLengthLimit` field is added to the `"go.opentelemetry.io/otel/sdk/trace".SpanLimits` type to configure this limit for a `TracerProvider`.
3132
The default limit for this resource is "unlimited". (#2637)

sdk/resource/config.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,3 +171,16 @@ func WithProcessRuntimeVersion() Option {
171171
func WithProcessRuntimeDescription() Option {
172172
return WithDetectors(processRuntimeDescriptionDetector{})
173173
}
174+
175+
// WithContainer adds all the Container attributes to the configured Resource.
176+
// See individual WithContainer* functions to configure specific attributes.
177+
func WithContainer() Option {
178+
return WithDetectors(
179+
cgroupContainerIDDetector{},
180+
)
181+
}
182+
183+
// WithContainerID adds an attribute with the id of the container to the configured Resource.
184+
func WithContainerID() Option {
185+
return WithDetectors(cgroupContainerIDDetector{})
186+
}

sdk/resource/container.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Copyright The OpenTelemetry Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package resource // import "go.opentelemetry.io/otel/sdk/resource"
16+
17+
import (
18+
"bufio"
19+
"context"
20+
"errors"
21+
"io"
22+
"os"
23+
"regexp"
24+
25+
semconv "go.opentelemetry.io/otel/semconv/v1.7.0"
26+
)
27+
28+
type containerIDProvider func() (string, error)
29+
30+
var (
31+
containerID containerIDProvider = getContainerIDFromCGroup
32+
cgroupContainerIDRe = regexp.MustCompile(`^.*/(?:.*-)?([0-9a-f]+)(?:\.|\s*$)`)
33+
)
34+
35+
type cgroupContainerIDDetector struct{}
36+
37+
const cgroupPath = "/proc/self/cgroup"
38+
39+
// Detect returns a *Resource that describes the id of the container.
40+
// If no container id found, an empty resource will be returned.
41+
func (cgroupContainerIDDetector) Detect(ctx context.Context) (*Resource, error) {
42+
containerID, err := containerID()
43+
if err != nil {
44+
return nil, err
45+
}
46+
47+
if containerID == "" {
48+
return Empty(), nil
49+
}
50+
return NewWithAttributes(semconv.SchemaURL, semconv.ContainerIDKey.String(containerID)), nil
51+
}
52+
53+
var (
54+
defaultOSStat = os.Stat
55+
osStat = defaultOSStat
56+
57+
defaultOSOpen = func(name string) (io.ReadCloser, error) {
58+
return os.Open(name)
59+
}
60+
osOpen = defaultOSOpen
61+
)
62+
63+
// getContainerIDFromCGroup returns the id of the container from the cgroup file.
64+
// If no container id found, an empty string will be returned.
65+
func getContainerIDFromCGroup() (string, error) {
66+
if _, err := osStat(cgroupPath); errors.Is(err, os.ErrNotExist) {
67+
// File does not exist, skip
68+
return "", nil
69+
}
70+
71+
file, err := osOpen(cgroupPath)
72+
if err != nil {
73+
return "", err
74+
}
75+
defer file.Close()
76+
77+
return getContainerIDFromReader(file), nil
78+
}
79+
80+
// getContainerIDFromReader returns the id of the container from reader.
81+
func getContainerIDFromReader(reader io.Reader) string {
82+
scanner := bufio.NewScanner(reader)
83+
for scanner.Scan() {
84+
line := scanner.Text()
85+
86+
if id := getContainerIDFromLine(line); id != "" {
87+
return id
88+
}
89+
}
90+
return ""
91+
}
92+
93+
// getContainerIDFromLine returns the id of the container from one string line.
94+
func getContainerIDFromLine(line string) string {
95+
matches := cgroupContainerIDRe.FindStringSubmatch(line)
96+
if len(matches) <= 1 {
97+
return ""
98+
}
99+
return matches[1]
100+
}

sdk/resource/container_test.go

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
// Copyright The OpenTelemetry Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package resource
16+
17+
import (
18+
"errors"
19+
"io"
20+
"os"
21+
"strings"
22+
"testing"
23+
24+
"github.com/stretchr/testify/assert"
25+
)
26+
27+
func setDefaultContainerProviders() {
28+
setContainerProviders(
29+
getContainerIDFromCGroup,
30+
)
31+
}
32+
33+
func setContainerProviders(
34+
idProvider containerIDProvider,
35+
) {
36+
containerID = idProvider
37+
}
38+
39+
func TestGetContainerIDFromLine(t *testing.T) {
40+
testCases := []struct {
41+
name string
42+
line string
43+
expectedContainerID string
44+
}{
45+
{
46+
name: "with suffix",
47+
line: "13:name=systemd:/podruntime/docker/kubepods/ac679f8a8319c8cf7d38e1adf263bc08d23.aaaa",
48+
expectedContainerID: "ac679f8a8319c8cf7d38e1adf263bc08d23",
49+
},
50+
{
51+
name: "with prefix and suffix",
52+
line: "13:name=systemd:/podruntime/docker/kubepods/crio-dc679f8a8319c8cf7d38e1adf263bc08d23.stuff",
53+
expectedContainerID: "dc679f8a8319c8cf7d38e1adf263bc08d23",
54+
},
55+
{
56+
name: "no prefix and suffix",
57+
line: "13:name=systemd:/pod/d86d75589bf6cc254f3e2cc29debdf85dde404998aa128997a819ff991827356",
58+
expectedContainerID: "d86d75589bf6cc254f3e2cc29debdf85dde404998aa128997a819ff991827356",
59+
},
60+
{
61+
name: "with space",
62+
line: " 13:name=systemd:/pod/d86d75589bf6cc254f3e2cc29debdf85dde404998aa128997a819ff991827356 ",
63+
expectedContainerID: "d86d75589bf6cc254f3e2cc29debdf85dde404998aa128997a819ff991827356",
64+
},
65+
{
66+
name: "invalid hex string",
67+
line: "13:name=systemd:/podruntime/docker/kubepods/ac679f8a8319c8cf7d38e1adf263bc08d23zzzz",
68+
},
69+
{
70+
name: "no container id - 1",
71+
line: "pids: /",
72+
},
73+
{
74+
name: "no container id - 2",
75+
line: "pids: ",
76+
},
77+
}
78+
79+
for _, tc := range testCases {
80+
t.Run(tc.name, func(t *testing.T) {
81+
containerID := getContainerIDFromLine(tc.line)
82+
assert.Equal(t, tc.expectedContainerID, containerID)
83+
})
84+
}
85+
}
86+
87+
func TestGetContainerIDFromReader(t *testing.T) {
88+
testCases := []struct {
89+
name string
90+
reader io.Reader
91+
expectedContainerID string
92+
}{
93+
{
94+
name: "multiple lines",
95+
reader: strings.NewReader(`//
96+
1:name=systemd:/podruntime/docker/kubepods/docker-dc579f8a8319c8cf7d38e1adf263bc08d23
97+
1:name=systemd:/podruntime/docker/kubepods/docker-dc579f8a8319c8cf7d38e1adf263bc08d24
98+
`),
99+
expectedContainerID: "dc579f8a8319c8cf7d38e1adf263bc08d23",
100+
},
101+
{
102+
name: "no container id",
103+
reader: strings.NewReader(`//
104+
1:name=systemd:/podruntime/docker
105+
`),
106+
expectedContainerID: "",
107+
},
108+
}
109+
110+
for _, tc := range testCases {
111+
t.Run(tc.name, func(t *testing.T) {
112+
containerID := getContainerIDFromReader(tc.reader)
113+
assert.Equal(t, tc.expectedContainerID, containerID)
114+
})
115+
}
116+
}
117+
118+
func TestGetContainerIDFromCGroup(t *testing.T) {
119+
t.Cleanup(func() {
120+
osStat = defaultOSStat
121+
osOpen = defaultOSOpen
122+
})
123+
124+
testCases := []struct {
125+
name string
126+
cgroupFileNotExist bool
127+
openFileError error
128+
content string
129+
expectedContainerID string
130+
expectedError bool
131+
}{
132+
{
133+
name: "the cgroup file does not exist",
134+
cgroupFileNotExist: true,
135+
},
136+
{
137+
name: "error when opening cgroup file",
138+
openFileError: errors.New("test"),
139+
expectedError: true,
140+
},
141+
{
142+
name: "cgroup file",
143+
content: "1:name=systemd:/podruntime/docker/kubepods/docker-dc579f8a8319c8cf7d38e1adf263bc08d23",
144+
expectedContainerID: "dc579f8a8319c8cf7d38e1adf263bc08d23",
145+
},
146+
}
147+
148+
for _, tc := range testCases {
149+
t.Run(tc.name, func(t *testing.T) {
150+
osStat = func(name string) (os.FileInfo, error) {
151+
if tc.cgroupFileNotExist {
152+
return nil, os.ErrNotExist
153+
}
154+
return nil, nil
155+
}
156+
157+
osOpen = func(name string) (io.ReadCloser, error) {
158+
if tc.openFileError != nil {
159+
return nil, tc.openFileError
160+
}
161+
return io.NopCloser(strings.NewReader(tc.content)), nil
162+
}
163+
164+
containerID, err := getContainerIDFromCGroup()
165+
assert.Equal(t, tc.expectedError, err != nil)
166+
assert.Equal(t, tc.expectedContainerID, containerID)
167+
})
168+
}
169+
}

sdk/resource/export_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ var (
2323
SetUserProviders = setUserProviders
2424
SetDefaultOSDescriptionProvider = setDefaultOSDescriptionProvider
2525
SetOSDescriptionProvider = setOSDescriptionProvider
26+
SetDefaultContainerProviders = setDefaultContainerProviders
27+
SetContainerProviders = setContainerProviders
2628
)
2729

2830
var (

sdk/resource/process_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,13 +102,14 @@ func restoreAttributesProviders() {
102102
resource.SetDefaultRuntimeProviders()
103103
resource.SetDefaultUserProviders()
104104
resource.SetDefaultOSDescriptionProvider()
105+
resource.SetDefaultContainerProviders()
105106
}
106107

107108
func TestWithProcessFuncsErrors(t *testing.T) {
108109
mockProcessAttributesProvidersWithErrors()
109110

110-
t.Run("WithPID", testWithProcessExecutablePathError)
111-
t.Run("WithExecutableName", testWithProcessOwnerError)
111+
t.Run("WithExecutablePath", testWithProcessExecutablePathError)
112+
t.Run("WithOwner", testWithProcessOwnerError)
112113

113114
restoreAttributesProviders()
114115
}

sdk/resource/resource_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,3 +649,74 @@ func hostname() string {
649649
}
650650
return hn
651651
}
652+
653+
func TestWithContainerID(t *testing.T) {
654+
t.Cleanup(restoreAttributesProviders)
655+
656+
fakeContainerID := "fake-container-id"
657+
658+
testCases := []struct {
659+
name string
660+
containerIDProvider func() (string, error)
661+
expectedResource map[string]string
662+
expectedErr bool
663+
}{
664+
{
665+
name: "get container id",
666+
containerIDProvider: func() (string, error) {
667+
return fakeContainerID, nil
668+
},
669+
expectedResource: map[string]string{
670+
string(semconv.ContainerIDKey): fakeContainerID,
671+
},
672+
},
673+
{
674+
name: "no container id found",
675+
containerIDProvider: func() (string, error) {
676+
return "", nil
677+
},
678+
expectedResource: map[string]string{},
679+
},
680+
{
681+
name: "error",
682+
containerIDProvider: func() (string, error) {
683+
return "", fmt.Errorf("unable to get container id")
684+
},
685+
expectedResource: map[string]string{},
686+
expectedErr: true,
687+
},
688+
}
689+
690+
for _, tc := range testCases {
691+
t.Run(tc.name, func(t *testing.T) {
692+
resource.SetContainerProviders(tc.containerIDProvider)
693+
694+
res, err := resource.New(context.Background(),
695+
resource.WithContainerID(),
696+
)
697+
698+
if tc.expectedErr {
699+
assert.Error(t, err)
700+
}
701+
assert.Equal(t, tc.expectedResource, toMap(res))
702+
})
703+
}
704+
}
705+
706+
func TestWithContainer(t *testing.T) {
707+
t.Cleanup(restoreAttributesProviders)
708+
709+
fakeContainerID := "fake-container-id"
710+
resource.SetContainerProviders(func() (string, error) {
711+
return fakeContainerID, nil
712+
})
713+
714+
res, err := resource.New(context.Background(),
715+
resource.WithContainer(),
716+
)
717+
718+
assert.NoError(t, err)
719+
assert.Equal(t, map[string]string{
720+
string(semconv.ContainerIDKey): fakeContainerID,
721+
}, toMap(res))
722+
}

0 commit comments

Comments
 (0)