Skip to content

Commit

Permalink
Added CGroupv2 support into Docker Extensions (open-telemetry#839)
Browse files Browse the repository at this point in the history
  • Loading branch information
anoopbabu29 authored and Michele Mancioppi committed Jan 11, 2023
1 parent 2f4179e commit e81267c
Show file tree
Hide file tree
Showing 2 changed files with 208 additions and 66 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -28,24 +29,49 @@ 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";

/// <summary>
/// CGroup Parse Versions.
/// </summary>
internal enum ParseMode
{
/// <summary>
/// Represents CGroupV1.
/// </summary>
V1,

/// <summary>
/// Represents CGroupV2.
/// </summary>
V2,
}

/// <summary>
/// Detects the resource attributes from Docker.
/// </summary>
/// <returns>Resource with key-value pairs of resource attributes.</returns>
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;
}

/// <summary>
/// Builds the resource attributes from Container Id in file path.
/// </summary>
/// <param name="path">File path where container id exists.</param>
/// <param name="cgroupVersion">CGroup Version of file to parse from.</param>
/// <returns>Returns Resource with list of key-value pairs of container resource attributes if container id exists else empty resource.</returns>
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))
{
Expand All @@ -58,11 +84,12 @@ internal Resource BuildResource(string path)
}

/// <summary>
/// Extracts Container Id from path.
/// Extracts Container Id from path using the cgroupv1 format.
/// </summary>
/// <param name="path">cgroup path.</param>
/// <param name="cgroupVersion">CGroup Version of file to parse from.</param>
/// <returns>Container Id, Null if not found or exception being thrown.</returns>
private string ExtractContainerId(string path)
private string ExtractContainerId(string path, ParseMode cgroupVersion)
{
try
{
Expand All @@ -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;
Expand All @@ -89,11 +128,11 @@ private string ExtractContainerId(string path)
}

/// <summary>
/// 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.
/// </summary>
/// <param name="line">line read from cgroup file.</param>
/// <returns>Container Id.</returns>
private string GetIdFromLine(string line)
/// <returns>Container Id, Null if not found.</returns>
private string GetIdFromLineV1(string line)
{
// This cgroup output line should have the container id in it
int lastSlashIndex = line.LastIndexOf('/');
Expand All @@ -116,6 +155,28 @@ private string GetIdFromLine(string line)
return containerId;
}

/// <summary>
/// Gets the Container Id from the line of the cgroupv2 format.
/// </summary>
/// <param name="line">line read from cgroup file.</param>
/// <returns>Container Id, Null if not found.</returns>
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
// limitations under the License.
// </copyright>

using System.Collections.Generic;
using System.IO;
using System.Linq;
using OpenTelemetry.Extensions.Docker.Resources;
Expand All @@ -24,88 +25,168 @@ 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<TestCase> 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<TestCase> 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<TestCase> 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)
{
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; }
}
}

0 comments on commit e81267c

Please sign in to comment.