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
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -30,6 +30,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)
- The `"go.opentelemetry.io/otel/sdk/resource".WithContainer` and `"go.opentelemetry.io/otel/sdk/resource".WithContainerID` support the cgoupv2 files. (#3508)
XSAM marked this conversation as resolved.
Show resolved Hide resolved

### Changed

Expand Down
41 changes: 24 additions & 17 deletions sdk/resource/container.go
Expand Up @@ -20,22 +20,20 @@ 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.
func (cgroupContainerIDDetector) Detect(ctx context.Context) (*Resource, error) {
Expand All @@ -61,8 +59,26 @@ var (
)

// 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.
XSAM marked this conversation as resolved.
Show resolved Hide resolved
// 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 {
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]
}
36 changes: 36 additions & 0 deletions sdk/resource/container_id_cgroup_v1.go
@@ -0,0 +1,36 @@
// 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"
)

const cgroupV1Path = "/proc/self/cgroup"

var cgroupV1ContainerIDRe = regexp.MustCompile(`^.*/(?:.*-)?([0-9a-f]+)(?:\.|\s*$)`)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is still the right way to capture this for V1.

When used with podman I'm capturing the userID of the pod, not a container or an empty string.


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

// getContainerIDFromCgroupV1Line returns the id of the container from one string line.
XSAM marked this conversation as resolved.
Show resolved Hide resolved
func getContainerIDFromCgroupV1Line(line string) string {
matches := cgroupV1ContainerIDRe.FindStringSubmatch(line)
if len(matches) <= 1 {
return ""
}
return matches[1]
}
69 changes: 69 additions & 0 deletions sdk/resource/container_id_cgroup_v1_test.go
@@ -0,0 +1,69 @@
// 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: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 := getContainerIDFromCgroupV1Line(tc.line)
assert.Equal(t, tc.expectedContainerID, containerID)
})
}
}
36 changes: 36 additions & 0 deletions sdk/resource/container_id_cgroup_v2.go
@@ -0,0 +1,36 @@
// 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"
)

const cgroupV2Path = "/proc/self/mountinfo"

var cgroupV2ContainerIDRe = regexp.MustCompile(`.*/docker/containers/([0-9a-f]{64})/.*`)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very docker specific. I found with podman there is a different format for the container layers, and I am also checking containerd.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@svrnm Thanks for the heads-up. Looks like you pasted the same link twice, but it reads like the second one intended to link to a js discussion? Curious what the alt approach is...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


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

// getContainerIDFromCgroupV2Line returns the id of the container from one string line.
func getContainerIDFromCgroupV2Line(line string) string {
matches := cgroupV2ContainerIDRe.FindStringSubmatch(line)
if len(matches) <= 1 {
return ""
}
return matches[1]
}
64 changes: 64 additions & 0 deletions sdk/resource/container_id_cgroup_v2_test.go
@@ -0,0 +1,64 @@
// 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: "resolv.conf",
line: "472 456 254:1 /docker/containers/dc64b5743252dbaef6e30521c34d6bbd1620c8ce65bdb7bf9e7143b61bb5b183/resolv.conf /etc/resolv.conf rw,relatime - ext4 /dev/vda1 rw",
expectedContainerID: "dc64b5743252dbaef6e30521c34d6bbd1620c8ce65bdb7bf9e7143b61bb5b183",
},
{
name: "hostname",
line: "473 456 254:1 /docker/containers/dc64b5743252dbaef6e30521c34d6bbd1620c8ce65bdb7bf9e7143b61bb5b183/hostname /etc/hostname rw,relatime - ext4 /dev/vda1 rw",
expectedContainerID: "dc64b5743252dbaef6e30521c34d6bbd1620c8ce65bdb7bf9e7143b61bb5b183",
},
{
name: "host",
line: "474 456 254:1 /docker/containers/dc64b5743252dbaef6e30521c34d6bbd1620c8ce65bdb7bf9e7143b61bb5b183/hosts /etc/hosts rw,relatime - ext4 /dev/vda1 rw",
expectedContainerID: "dc64b5743252dbaef6e30521c34d6bbd1620c8ce65bdb7bf9e7143b61bb5b183",
},
}

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