diff --git a/src/OpenTelemetry.Extensions.Docker/Resources/DockerResourceDetector.cs b/src/OpenTelemetry.Extensions.Docker/Resources/DockerResourceDetector.cs index fb6d25f452..a589117d37 100644 --- a/src/OpenTelemetry.Extensions.Docker/Resources/DockerResourceDetector.cs +++ b/src/OpenTelemetry.Extensions.Docker/Resources/DockerResourceDetector.cs @@ -17,6 +17,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text.RegularExpressions; using OpenTelemetry.Extensions.Docker.Utils; using OpenTelemetry.Resources; @@ -28,6 +29,24 @@ namespace OpenTelemetry.Extensions.Docker.Resources; public class DockerResourceDetector : IResourceDetector { private const string FILEPATH = "/proc/self/cgroup"; + private const string FILEPATHV2 = "/proc/self/mountinfo"; + private const string HOSTNAME = "hostname"; + + /// + /// CGroup Parse Versions. + /// + internal enum ParseMode + { + /// + /// Represents CGroupV1. + /// + V1, + + /// + /// Represents CGroupV2. + /// + V2, + } /// /// Detects the resource attributes from Docker. @@ -35,17 +54,24 @@ public class DockerResourceDetector : IResourceDetector /// Resource with key-value pairs of resource attributes. public Resource Detect() { - return this.BuildResource(FILEPATH); + var cGroupBuild = this.BuildResource(FILEPATH, ParseMode.V1); + if (cGroupBuild == Resource.Empty) + { + cGroupBuild = this.BuildResource(FILEPATHV2, ParseMode.V2); + } + + return cGroupBuild; } /// /// Builds the resource attributes from Container Id in file path. /// /// File path where container id exists. + /// CGroup Version of file to parse from. /// Returns Resource with list of key-value pairs of container resource attributes if container id exists else empty resource. - internal Resource BuildResource(string path) + internal Resource BuildResource(string path, ParseMode cgroupVersion) { - var containerId = this.ExtractContainerId(path); + var containerId = this.ExtractContainerId(path, cgroupVersion); if (string.IsNullOrEmpty(containerId)) { @@ -58,11 +84,12 @@ internal Resource BuildResource(string path) } /// - /// Extracts Container Id from path. + /// Extracts Container Id from path using the cgroupv1 format. /// /// cgroup path. + /// CGroup Version of file to parse from. /// Container Id, Null if not found or exception being thrown. - private string ExtractContainerId(string path) + private string ExtractContainerId(string path, ParseMode cgroupVersion) { try { @@ -73,7 +100,19 @@ private string ExtractContainerId(string path) foreach (string line in File.ReadLines(path)) { - string containerId = (!string.IsNullOrEmpty(line)) ? this.GetIdFromLine(line) : null; + string containerId = null; + if (!string.IsNullOrEmpty(line)) + { + if (cgroupVersion == ParseMode.V1) + { + containerId = this.GetIdFromLineV1(line); + } + else if (cgroupVersion == ParseMode.V2 && line.Contains(HOSTNAME)) + { + containerId = this.GetIdFromLineV2(line); + } + } + if (!string.IsNullOrEmpty(containerId)) { return containerId; @@ -89,11 +128,11 @@ private string ExtractContainerId(string path) } /// - /// Gets the Container Id from the line after removing the prefix and suffix. + /// Gets the Container Id from the line after removing the prefix and suffix from the cgroupv1 format. /// /// line read from cgroup file. - /// Container Id. - private string GetIdFromLine(string line) + /// Container Id, Null if not found. + private string GetIdFromLineV1(string line) { // This cgroup output line should have the container id in it int lastSlashIndex = line.LastIndexOf('/'); @@ -116,6 +155,28 @@ private string GetIdFromLine(string line) return containerId; } + /// + /// Gets the Container Id from the line of the cgroupv2 format. + /// + /// line read from cgroup file. + /// Container Id, Null if not found. + private string GetIdFromLineV2(string line) + { + string containerId = null; + var match = Regex.Match(line, @".*/.+/([\w+-.]{64})/.*$"); + if (match.Success) + { + containerId = match.Groups[1].Value; + } + + if (string.IsNullOrEmpty(containerId) || !EncodingUtils.IsValidHexString(containerId)) + { + return null; + } + + return containerId; + } + private string RemovePrefixAndSuffixIfneeded(string input, int startIndex, int endIndex) { startIndex = (startIndex == -1) ? 0 : startIndex + 1; diff --git a/test/OpenTelemetry.Extensions.Docker.Tests/Resources/DockerResourceDetectorTests.cs b/test/OpenTelemetry.Extensions.Docker.Tests/Resources/DockerResourceDetectorTests.cs index 004410766e..5c50045727 100644 --- a/test/OpenTelemetry.Extensions.Docker.Tests/Resources/DockerResourceDetectorTests.cs +++ b/test/OpenTelemetry.Extensions.Docker.Tests/Resources/DockerResourceDetectorTests.cs @@ -14,6 +14,7 @@ // limitations under the License. // +using System.Collections.Generic; using System.IO; using System.Linq; using OpenTelemetry.Extensions.Docker.Resources; @@ -24,83 +25,152 @@ namespace OpenTelemetry.Extensions.Docker.Tests; public class DockerResourceDetectorTests { - // Invalid cgroup line - private const string INVALIDCGROUPLINE = - "13:name=systemd:/podruntime/docker/kubepods/ac679f8a8319c8cf7d38e1adf263bc08d23zzzz"; - - // cgroup line with prefix - private const string CGROUPLINEWITHPREFIX = - "13:name=systemd:/podruntime/docker/kubepods/crio-e2cc29debdf85dde404998aa128997a819ff"; - - // Expected Container Id with prefix removed - private const string CONTAINERIDWITHPREFIXREMOVED = "e2cc29debdf85dde404998aa128997a819ff"; - - // cgroup line with suffix - private const string CGROUPLINEWITHSUFFIX = - "13:name=systemd:/podruntime/docker/kubepods/ac679f8a8319c8cf7d38e1adf263bc08d23.aaaa"; - - // Expected Container Id with suffix removed - private const string CONTAINERIDWITHSUFFIXREMOVED = "ac679f8a8319c8cf7d38e1adf263bc08d23"; - - // cgroup line with prefix and suffix - private const string CGROUPLINEWITHPREFIXandSUFFIX = - "13:name=systemd:/podruntime/docker/kubepods/crio-dc679f8a8319c8cf7d38e1adf263bc08d23.stuff"; - - // Expected Container Id with both prefix and suffix removed - private const string CONTAINERIDWITHPREFIXANDSUFFIXREMOVED = "dc679f8a8319c8cf7d38e1adf263bc08d23"; - - // cgroup line with container Id - private const string CGROUPLINE = - "13:name=systemd:/pod/d86d75589bf6cc254f3e2cc29debdf85dde404998aa128997a819ff991827356"; - - // Expected Container Id - private const string CONTAINERID = - "d86d75589bf6cc254f3e2cc29debdf85dde404998aa128997a819ff991827356"; + private readonly List testValidCasesV1 = new() + { + new TestCase() + { + Name = "cgroupv1 with prefix", + Line = "13:name=systemd:/podruntime/docker/kubepods/crio-e2cc29debdf85dde404998aa128997a819ff", + ExpectedContainerId = "e2cc29debdf85dde404998aa128997a819ff", + CgroupVersion = DockerResourceDetector.ParseMode.V1, + }, + new TestCase() + { + Name = "cgroupv1 with suffix", + Line = "13:name=systemd:/podruntime/docker/kubepods/ac679f8a8319c8cf7d38e1adf263bc08d23.aaaa", + ExpectedContainerId = "ac679f8a8319c8cf7d38e1adf263bc08d23", + CgroupVersion = DockerResourceDetector.ParseMode.V1, + }, + new TestCase() + { + Name = "cgroupv1 with prefix and suffix", + Line = "13:name=systemd:/podruntime/docker/kubepods/crio-dc679f8a8319c8cf7d38e1adf263bc08d23.stuff", + ExpectedContainerId = "dc679f8a8319c8cf7d38e1adf263bc08d23", + CgroupVersion = DockerResourceDetector.ParseMode.V1, + }, + new TestCase() + { + Name = "cgroupv1 with container Id", + Line = "13:name=systemd:/pod/d86d75589bf6cc254f3e2cc29debdf85dde404998aa128997a819ff991827356", + ExpectedContainerId = "d86d75589bf6cc254f3e2cc29debdf85dde404998aa128997a819ff991827356", + CgroupVersion = DockerResourceDetector.ParseMode.V1, + }, + }; + + private readonly List testValidCasesV2 = new() + { + new TestCase() + { + Name = "cgroupv2 with container Id", + Line = "13:name=systemd:/pod/d86d75589bf6cc254f3e2cc29debdf85dde404998aa128997a819ff991827356/hostname", + ExpectedContainerId = "d86d75589bf6cc254f3e2cc29debdf85dde404998aa128997a819ff991827356", + CgroupVersion = DockerResourceDetector.ParseMode.V2, + }, + new TestCase() + { + Name = "cgroupv2 with full line", + Line = "473 456 254:1 /docker/containers/dc64b5743252dbaef6e30521c34d6bbd1620c8ce65bdb7bf9e7143b61bb5b183/hostname /etc/hostname rw,relatime - ext4 /dev/vda1 rw", + ExpectedContainerId = "dc64b5743252dbaef6e30521c34d6bbd1620c8ce65bdb7bf9e7143b61bb5b183", + CgroupVersion = DockerResourceDetector.ParseMode.V2, + }, + new TestCase() + { + Name = "cgroupv2 with 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", + CgroupVersion = DockerResourceDetector.ParseMode.V2, + }, + new TestCase() + { + Name = "cgroupv2 with minikube docker mountinfo", + Line = "2327 2307 8:1 /var/lib/docker/containers/a1551a1d7e1881d6c18d2c9ec462cab6ad3666825f0adb2098e9d5b198fd7e19/hostname /etc/hostname rw,relatime - ext4 /dev/sda1 rw", + ExpectedContainerId = "a1551a1d7e1881d6c18d2c9ec462cab6ad3666825f0adb2098e9d5b198fd7e19", + CgroupVersion = DockerResourceDetector.ParseMode.V2, + }, + new TestCase() + { + Name = "cgroupv2 with minikube docker mountinfo2", + Line = "929 920 254:1 /docker/volumes/minikube/_data/lib/docker/containers/0eaa6718003210b6520f7e82d14b4c8d4743057a958a503626240f8d1900bc33/hostname /etc/hostname rw,relatime - ext4 /dev/vda1 rw", + ExpectedContainerId = "0eaa6718003210b6520f7e82d14b4c8d4743057a958a503626240f8d1900bc33", + CgroupVersion = DockerResourceDetector.ParseMode.V2, + }, + new TestCase() + { + Name = "cgroupv2 with 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", + CgroupVersion = DockerResourceDetector.ParseMode.V2, + }, + }; + + private readonly List testInvalidCases = new() + { + new TestCase() + { + Name = "Invalid cgroupv1 line", + Line = "13:name=systemd:/podruntime/docker/kubepods/ac679f8a8319c8cf7d38e1adf263bc08d23zzzz", + CgroupVersion = DockerResourceDetector.ParseMode.V1, + }, + new TestCase() + { + Name = "Invalid hex cgroupv2 line (contains a z)", + Line = "13:name=systemd:/var/lib/containerd/io.containerd.grpc.v1.cri/sandboxes/fb5916a02feca96bdeecd8e062df9e5e51d6617c8214b5e1f3fz9320f4402ae6/hostname", + CgroupVersion = DockerResourceDetector.ParseMode.V2, + }, + }; [Fact] public void TestValidContainer() { var dockerResourceDetector = new DockerResourceDetector(); + var allValidTestCases = this.testValidCasesV1.Concat(this.testValidCasesV2); - using (TempFile tempFile = new TempFile()) + foreach (var testCase in allValidTestCases) { - tempFile.Write(CGROUPLINEWITHPREFIX); - Assert.Equal(CONTAINERIDWITHPREFIXREMOVED, this.GetContainerId(dockerResourceDetector.BuildResource(tempFile.FilePath))); + using TempFile tempFile = new TempFile(); + tempFile.Write(testCase.Line); + Assert.Equal( + testCase.ExpectedContainerId, + this.GetContainerId(dockerResourceDetector.BuildResource(tempFile.FilePath, testCase.CgroupVersion))); } + } - using (TempFile tempFile = new TempFile()) - { - tempFile.Write(CGROUPLINEWITHSUFFIX); - Assert.Equal(CONTAINERIDWITHSUFFIXREMOVED, this.GetContainerId(dockerResourceDetector.BuildResource(tempFile.FilePath))); - } + [Fact] + public void TestInvalidContainer() + { + var dockerResourceDetector = new DockerResourceDetector(); - using (TempFile tempFile = new TempFile()) + // Valid in cgroupv1 is not valid in cgroupv2 + foreach (var testCase in this.testValidCasesV1) { - tempFile.Write(CGROUPLINEWITHPREFIXandSUFFIX); - Assert.Equal(CONTAINERIDWITHPREFIXANDSUFFIXREMOVED, this.GetContainerId(dockerResourceDetector.BuildResource(tempFile.FilePath))); + using TempFile tempFile = new TempFile(); + tempFile.Write(testCase.Line); + Assert.Equal( + dockerResourceDetector.BuildResource(tempFile.FilePath, DockerResourceDetector.ParseMode.V2), + Resource.Empty); } - using (TempFile tempFile = new TempFile()) + // Valid in cgroupv1 is not valid in cgroupv1 + foreach (var testCase in this.testValidCasesV2) { - tempFile.Write(CGROUPLINE); - Assert.Equal(CONTAINERID, this.GetContainerId(dockerResourceDetector.BuildResource(tempFile.FilePath))); + using TempFile tempFile = new TempFile(); + tempFile.Write(testCase.Line); + Assert.Equal( + dockerResourceDetector.BuildResource(tempFile.FilePath, DockerResourceDetector.ParseMode.V1), + Resource.Empty); } - } - [Fact] - public void TestInvalidContainer() - { - var dockerResourceDetector = new DockerResourceDetector(); - - // test invalid containerId (non-hex) - using (TempFile tempFile = new TempFile()) + // test invalid cases + foreach (var testCase in this.testInvalidCases) { - tempFile.Write(INVALIDCGROUPLINE); - Assert.Equal(dockerResourceDetector.BuildResource(tempFile.FilePath), Resource.Empty); + using TempFile tempFile = new TempFile(); + tempFile.Write(testCase.Line); + Assert.Equal(dockerResourceDetector.BuildResource(tempFile.FilePath, testCase.CgroupVersion), Resource.Empty); } // test invalid file - Assert.Equal(dockerResourceDetector.BuildResource(Path.GetTempPath()), Resource.Empty); + Assert.Equal(dockerResourceDetector.BuildResource(Path.GetTempPath(), DockerResourceDetector.ParseMode.V1), Resource.Empty); + Assert.Equal(dockerResourceDetector.BuildResource(Path.GetTempPath(), DockerResourceDetector.ParseMode.V2), Resource.Empty); } private string GetContainerId(Resource resource) @@ -108,4 +178,15 @@ private string GetContainerId(Resource resource) var resourceAttributes = resource.Attributes.ToDictionary(x => x.Key, x => x.Value); return resourceAttributes[DockerSemanticConventions.AttributeContainerID]?.ToString(); } + + private class TestCase + { + public string Name { get; set; } + + public string Line { get; set; } + + public string ExpectedContainerId { get; set; } + + public DockerResourceDetector.ParseMode CgroupVersion { get; set; } + } }