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 cgroup v2 support for container id detector #3508

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -53,6 +53,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
These additions are replacements for the `Instrument` and `InstrumentKind` types from `go.opentelemetry.io/otel/sdk/metric/view`. (#3459)
- The `Stream` type is added to `go.opentelemetry.io/otel/sdk/metric` to define a metric data stream a view will produce. (#3459)
- The `AssertHasAttributes` allows instrument authors to test that datapoints returned have appropriate attributes. (#3487)
- Both `"go.opentelemetry.io/otel/sdk/resource".WithContainer` and `"go.opentelemetry.io/otel/sdk/resource".WithContainerID` now support the cgoupv2 files. (#3508)

### Changed

Expand All @@ -79,6 +80,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Handle partial success responses in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric` exporters. (#3162, #3440)
- Prevent duplicate Prometheus description, unit, and type. (#3469)
- Prevents panic when using incorrect `attribute.Value.As[Type]Slice()`. (#3489)
- Both `"go.opentelemetry.io/otel/sdk/resource".WithContainer` and `"go.opentelemetry.io/otel/sdk/resource".WithContainerID` now return the correct container ID on the Podman. (#3508)

### Removed

Expand Down
51 changes: 29 additions & 22 deletions sdk/resource/container.go
Expand Up @@ -20,24 +20,22 @@ import (
"errors"
"io"
"os"
"regexp"

semconv "go.opentelemetry.io/otel/semconv/v1.12.0"
)

type containerIDProvider func() (string, error)

var (
containerID containerIDProvider = getContainerIDFromCGroup
cgroupContainerIDRe = regexp.MustCompile(`^.*/(?:.*-)?([0-9a-f]+)(?:\.|\s*$)`)
containerID containerIDProvider = getContainerIDFromCGroup
cgroupV1ContainerIDProvider containerIDProvider = getContainerIDFromCGroupV1
cgroupV2ContainerIDProvider containerIDProvider = getContainerIDFromCGroupV2
)

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.
// 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 {
Expand All @@ -60,9 +58,27 @@ var (
osOpen = defaultOSOpen
)

// getContainerIDFromCGroup returns the id of the container from the cgroup file.
// If no container id found, an empty string will be returned.
// getContainerIDFromCGroup returns the ID of the container from the cgroup file.
// If cgroup v1 container ID provider fails, then fall back to cgroup v2 container ID provider.
// If no container ID found, an empty string will be returned.
func getContainerIDFromCGroup() (string, error) {
containerID, err := cgroupV1ContainerIDProvider()
if err != nil {
return "", err
}

if containerID == "" {
// Fallback to cgroup v2
containerID, err = cgroupV2ContainerIDProvider()
if err != nil {
return "", err
}
}

return containerID, nil
}

func getContainerIDFromCGroupFile(cgroupPath string, extractor func(string) string) (string, error) {
if _, err := osStat(cgroupPath); errors.Is(err, os.ErrNotExist) {
// File does not exist, skip
return "", nil
Expand All @@ -74,27 +90,18 @@ func getContainerIDFromCGroup() (string, error) {
}
defer file.Close()

return getContainerIDFromReader(file), nil
return getContainerIDFromReader(file, extractor), nil
}

// getContainerIDFromReader returns the id of the container from reader.
func getContainerIDFromReader(reader io.Reader) string {
// getContainerIDFromReader returns the ID of the container from reader.
func getContainerIDFromReader(reader io.Reader, extractor func(string) string) string {
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
line := scanner.Text()

if id := getContainerIDFromLine(line); id != "" {
if id := extractor(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]
}
42 changes: 42 additions & 0 deletions sdk/resource/container_id_cgroup_v1.go
@@ -0,0 +1,42 @@
// 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 (
"regexp"
"strings"
)

const cgroupV1Path = "/proc/self/cgroup"

var cgroupV1ContainerIDRe = regexp.MustCompile(`^.*/(?:.*-)?([\w+-]+)(?:\.|\s*$)`)

func getContainerIDFromCGroupV1() (string, error) {
return getContainerIDFromCGroupFile(cgroupV1Path, getContainerIDFromCgroupV1Line)
}

// getContainerIDFromCgroupV1Line returns the ID of the container from one string line.
func getContainerIDFromCgroupV1Line(line string) string {
// Only match line contains "cpuset"
if !strings.Contains(line, "cpuset") {
return ""
}

matches := cgroupV1ContainerIDRe.FindStringSubmatch(line)
if len(matches) <= 1 {
return ""
}
return matches[1]
}
83 changes: 83 additions & 0 deletions sdk/resource/container_id_cgroup_v1_test.go
@@ -0,0 +1,83 @@
// 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 (
"testing"

"github.com/stretchr/testify/assert"
)

func TestGetContainerIDFromCgroupV1Line(t *testing.T) {
testCases := []struct {
name string
line string
expectedContainerID string
}{
{
name: "with suffix",
line: "13:cpuset:/podruntime/docker/kubepods/ac679f8a8319c8cf7d38e1adf263bc08d23.aaaa",
expectedContainerID: "ac679f8a8319c8cf7d38e1adf263bc08d23",
},
{
name: "with prefix and suffix",
line: "13:cpuset:/podruntime/docker/kubepods/crio-dc679f8a8319c8cf7d38e1adf263bc08d23.stuff",
expectedContainerID: "dc679f8a8319c8cf7d38e1adf263bc08d23",
},
{
name: "no prefix and suffix",
line: "13:cpuset:/pod/d86d75589bf6cc254f3e2cc29debdf85dde404998aa128997a819ff991827356",
expectedContainerID: "d86d75589bf6cc254f3e2cc29debdf85dde404998aa128997a819ff991827356",
},
{
name: "with space",
line: " 13:cpuset:/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: ",
},
{
name: "minikube containerd cgroup",
line: "11:cpuset:/kubepods/besteffort/pod28478e30-384f-41e5-9d85-eae249ae8506/58a77afcbf0b16959d526758f6696677c862517acc97a562dc5c5b09afbf5236",
expectedContainerID: "58a77afcbf0b16959d526758f6696677c862517acc97a562dc5c5b09afbf5236",
},
{
name: "minikube docker cgroup",
line: "5:cpuset:/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod350bff31_89d4_429e_b653_86d8167bc60e.slice/docker-3a5881f09ab409d7ac174b59f20d003b28b76da368257eb1e3d23648920a742b.scope",
expectedContainerID: "3a5881f09ab409d7ac174b59f20d003b28b76da368257eb1e3d23648920a742b",
},
{
name: "podman cgroup",
line: "14:name=systemd:/user.slice/user-1000.slice/user@1000.service/app.slice/podman.service",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
containerID := getContainerIDFromCgroupV1Line(tc.line)
assert.Equal(t, tc.expectedContainerID, containerID)
})
}
}
42 changes: 42 additions & 0 deletions sdk/resource/container_id_cgroup_v2.go
@@ -0,0 +1,42 @@
// 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 (
"regexp"
"strings"
)

const cgroupV2Path = "/proc/self/mountinfo"

var cgroupV2ContainerIDRe = regexp.MustCompile(`^.*/.+/([\w+-.]{64})/.*$`)

func getContainerIDFromCGroupV2() (string, error) {
return getContainerIDFromCGroupFile(cgroupV2Path, getContainerIDFromCgroupV2Line)
}

// getContainerIDFromCgroupV2Line returns the ID of the container from one string line.
func getContainerIDFromCgroupV2Line(line string) string {
// Only match line contains "cpuset"
if !strings.Contains(line, "hostname") {
return ""
}

matches := cgroupV2ContainerIDRe.FindStringSubmatch(line)
if len(matches) <= 1 {
return ""
}
return matches[1]
}
74 changes: 74 additions & 0 deletions sdk/resource/container_id_cgroup_v2_test.go
@@ -0,0 +1,74 @@
// 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 (
"testing"

"github.com/stretchr/testify/assert"
)

func TestGetContainerIDFromCgroupV2Line(t *testing.T) {
testCases := []struct {
name string
line string
expectedContainerID string
}{
{
name: "empty - 1",
line: "456 375 0:143 / / rw,relatime master:175 - overlay overlay rw,lowerdir=/var/lib/docker/overlay2/l/37L57D2IM7MEWLVE2Q2ECNDT67:/var/lib/docker/overlay2/l/46FCA2JFPCSNFGAR5TSYLLNHLK,upperdir=/var/lib/docker/overlay2/4e82c300793d703c19bdf887bfdad8b0354edda884ea27a8a2df89ab292719a4/diff,workdir=/var/lib/docker/overlay2/4e82c300793d703c19bdf887bfdad8b0354edda884ea27a8a2df89ab292719a4/work",
},
{
name: "empty - 2",
line: "457 456 0:146 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw",
},
{
name: "empty - 3",
line: "383 457 0:147 /null /proc/kcore rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755",
},
{
name: "hostname",
line: "473 456 254:1 /docker/containers/dc64b5743252dbaef6e30521c34d6bbd1620c8ce65bdb7bf9e7143b61bb5b183/hostname /etc/hostname rw,relatime - ext4 /dev/vda1 rw",
expectedContainerID: "dc64b5743252dbaef6e30521c34d6bbd1620c8ce65bdb7bf9e7143b61bb5b183",
},
{
name: "minikube containerd mountinfo",
line: "1537 1517 8:1 /var/lib/containerd/io.containerd.grpc.v1.cri/sandboxes/fb5916a02feca96bdeecd8e062df9e5e51d6617c8214b5e1f3ff9320f4402ae6/hostname /etc/hostname rw,relatime - ext4 /dev/sda1 rw",
expectedContainerID: "fb5916a02feca96bdeecd8e062df9e5e51d6617c8214b5e1f3ff9320f4402ae6",
},
{
name: "minikube docker mountinfo",
line: "2327 2307 8:1 /var/lib/docker/containers/a1551a1d7e1881d6c18d2c9ec462cab6ad3666825f0adb2098e9d5b198fd7e19/hostname /etc/hostname rw,relatime - ext4 /dev/sda1 rw",
expectedContainerID: "a1551a1d7e1881d6c18d2c9ec462cab6ad3666825f0adb2098e9d5b198fd7e19",
},
{
name: "minikube docker mountinfo 2",
line: "929 920 254:1 /docker/volumes/minikube/_data/lib/docker/containers/0eaa6718003210b6520f7e82d14b4c8d4743057a958a503626240f8d1900bc33/hostname /etc/hostname rw,relatime - ext4 /dev/vda1 rw",
expectedContainerID: "0eaa6718003210b6520f7e82d14b4c8d4743057a958a503626240f8d1900bc33",
},
{
name: "podman mountinfo",
line: "1096 1088 0:104 /containers/overlay-containers/1a2de27e7157106568f7e081e42a8c14858c02bd9df30d6e352b298178b46809/userdata/hostname /etc/hostname rw,nosuid,nodev,relatime - tmpfs tmpfs rw,size=813800k,nr_inodes=203450,mode=700,uid=1000,gid=1000",
expectedContainerID: "1a2de27e7157106568f7e081e42a8c14858c02bd9df30d6e352b298178b46809",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
containerID := getContainerIDFromCgroupV2Line(tc.line)
assert.Equal(t, tc.expectedContainerID, containerID)
})
}
}