Skip to content

Commit

Permalink
Update test cases to cover new format and fallbacks to mountinfo
Browse files Browse the repository at this point in the history
  • Loading branch information
Mpdreamz committed Jun 5, 2024
1 parent 63885e3 commit a36f868
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 15 deletions.
61 changes: 56 additions & 5 deletions src/Elastic.Apm/Helpers/SystemInfoHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System;
using System.Data.Common;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Text.RegularExpressions;
Expand All @@ -28,6 +29,27 @@ internal class SystemInfoHelper
public SystemInfoHelper(IApmLogger logger)
=> _logger = logger.Scoped(nameof(SystemInfoHelper));


//3997 3984 253:1 /var/lib/docker/containers/6548c6863fb748e72d1e2a4f824fde92f720952d062dede1318c2d6219a672d6/hostname /etc/hostname rw,relatime shared:1877 - ext4 /dev/mapper/vgubuntu-root rw,errors=remount-ro
internal void ParseMountInfo(Api.System system, string reportedHostName, string line)
{

var fields = line.Split(' ');
if (fields.Length <= 3)
return;

var path = fields[3];
foreach (var folder in path.Split('/'))
{
//naive implementation to check for guid.
if (folder.Length != 64) continue;
system.Container = new Container { Id = folder };
}

}

// "1:name=systemd:/ecs/03752a671e744971a862edcee6195646/03752a671e744971a862edcee6195646-4015103728"
// "0::/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod121157b5_c67d_4c3e_9052_cb27bbb711fb.slice/cri-containerd-1cd3449e930b8a28c7595240fa32ba20c84f36d059e5fbe63104ad40057992d1.scope"
internal void ParseContainerId(Api.System system, string reportedHostName, string line)
{
var fields = line.Split(':');
Expand All @@ -43,11 +65,13 @@ internal void ParseContainerId(Api.System system, string reportedHostName, strin
if (string.IsNullOrWhiteSpace(idPart))
return;

// Legacy, e.g.: /system.slice/docker-<CID>.scope
// Legacy, e.g.: /system.slice/docker-<CID>.scope or cri-containerd-<CID>.scope
if (idPart.EndsWith(".scope"))
{
idPart = idPart.Substring(0, idPart.Length - ".scope".Length)
.Substring(idPart.IndexOf("-", StringComparison.Ordinal) + 1);
var idParts = idPart.Split(new[] { '-'}, StringSplitOptions.RemoveEmptyEntries);
var containerIdWithScope = idParts.Last();

idPart = containerIdWithScope.Substring(0, containerIdWithScope.Length - ".scope".Length);
}

// Looking for kubernetes info
Expand Down Expand Up @@ -173,8 +197,10 @@ static string NormalizeHostName(string hostName) =>

private void ParseContainerInfo(Api.System system, string reportedHostName)
{
//0::/
try
{
var fallBackToMountInfo = false;
using var sr = GetCGroupAsStream();
if (sr is null)
{
Expand All @@ -183,12 +209,33 @@ private void ParseContainerInfo(Api.System system, string reportedHostName)
return;
}

var i = 0;
string line;
while ((line = sr.ReadLine()) != null)
{
if (line == "0::/" && i == 0)
fallBackToMountInfo = true;
ParseContainerId(system, reportedHostName, line);
if (system.Container != null)
return;
i++;
}
if (!fallBackToMountInfo)
return;

using var mi = GetMountInfoAsStream();
if (mi is null)
{
_logger.Debug()?.Log("No /proc/self/mountinfo found - no information to fallback to");
return;
}

while ((line = mi.ReadLine()) != null)
{
if (!line.Contains("/etc/hostname")) continue;
ParseMountInfo(system, reportedHostName, line);
if (system.Container != null)
return;
}
}
catch (Exception e)
Expand All @@ -201,8 +248,12 @@ private void ParseContainerInfo(Api.System system, string reportedHostName)
"Failed parsing container id - the agent will not report container id. Likely the application is not running within a container");
}

protected virtual StreamReader GetCGroupAsStream()
=> File.Exists("/proc/self/cgroup") ? new StreamReader("/proc/self/cgroup") : null;
protected virtual StreamReader GetCGroupAsStream() =>
File.Exists("/proc/self/cgroup") ? new StreamReader("/proc/self/cgroup") : null;

protected virtual StreamReader GetMountInfoAsStream() =>
File.Exists("/proc/self/mountinfo") ? new StreamReader("/proc/self/mountinfo") : null;


internal const string Namespace = "KUBERNETES_NAMESPACE";
internal const string PodName = "KUBERNETES_POD_NAME";
Expand Down
78 changes: 78 additions & 0 deletions test/Elastic.Apm.Tests.Utilities/CGroupTestCasesAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Licensed to Elasticsearch B.V under
// one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Elastic.Apm.Libraries.Newtonsoft.Json.Linq;
using Xunit.Sdk;

namespace Elastic.Apm.Tests.Utilities;

public struct CgroupFiles
{
public string ProcSelfCgroup;
public string[] MountInfo;
}

public struct CGroupTestData
{
public CgroupFiles Files;
public string ContainerId;
public string PodId;
}


public class CGroupTestCasesAttribute : DataAttribute
{
private readonly string _fileName = "./TestResources/json-specs//container_metadata_discovery.json";

public override IEnumerable<object[]> GetData(MethodInfo testMethod)
{
if (!File.Exists(_fileName))
throw new ArgumentException($"JSON input file {_fileName} does not exist");

var jToken = JToken.Parse(File.ReadAllText(_fileName), new JsonLoadSettings
{
CommentHandling = CommentHandling.Ignore
});

foreach (var kvp in (JObject)jToken)
{
var name = kvp.Key;
var data = ParseTestData(kvp.Value as JObject);
yield return [name, data];
}
}

private static CGroupTestData ParseTestData(JObject jToken)
{
var testData = new CGroupTestData { Files = new CgroupFiles() };

foreach (var kvp in jToken)
{
switch (kvp.Key)
{
case "containerId":
testData.ContainerId = kvp.Value?.Value<string>();
break;
case "podId":
testData.PodId = kvp.Value?.Value<string>();
break;
case "files":
var o = (JObject)kvp.Value;
var cgroupA = o.Property("/proc/self/cgroup")?.Value as JArray;
testData.Files.ProcSelfCgroup = cgroupA?.Values<string>().FirstOrDefault();

var mountInfoA = o.Property("/proc/self/mountinfo")?.Value as JArray;
testData.Files.MountInfo = mountInfoA?.Values<string>().ToArray();
break;
}
}
return testData;
}
}
25 changes: 15 additions & 10 deletions test/Elastic.Apm.Tests/SystemInfoHelperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@

using System;
using System.Collections.Generic;
using System.Linq;
using Elastic.Apm.Api.Kubernetes;
using Elastic.Apm.Features;
using Elastic.Apm.Helpers;
using Elastic.Apm.Logging;
using Elastic.Apm.Tests.Utilities;
using FluentAssertions;
using Newtonsoft.Json;
using Xunit;

namespace Elastic.Apm.Tests;
Expand Down Expand Up @@ -61,30 +63,33 @@ public void ParseKubernetesInfo_ShouldReturnNull_WhenNoEnvironmentVariablesAreSe
system.Kubernetes.Should().BeNull();
}

public struct CGroupTestData
{
public string GroupLine;
public string ContainerId;
public string PodId;
}

// Remove warning about unused test parameter "name"
#pragma warning disable xUnit1026
[Theory]
[JsonFileData("./TestResources/json-specs/cgroup_parsing.json", typeof(CGroupTestData))]
[CGroupTestCases]
public void ParseKubernetesInfo_FromCGroupLine(string name, CGroupTestData data)
{
var line = data.GroupLine;
data.Files.ProcSelfCgroup.Should().NotBeNull();
var line = data.Files.ProcSelfCgroup;
var containerId = data.ContainerId;
var podId = data.PodId;

var system = new Api.System();
_systemInfoHelper.ParseContainerId(system, "hostname", line);
if (line == "0::/")
{
line = data.Files.MountInfo.FirstOrDefault(l => l.Contains("/etc/hostname"));
_systemInfoHelper.ParseMountInfo(system, "hostname", line);
}
else
_systemInfoHelper.ParseContainerId(system, "hostname", line);

if (containerId is null)
system.Container.Should().BeNull();
else
{
system.Container.Should().NotBeNull("{0}", line);
system.Container.Id.Should().Be(containerId);
}

if (podId is null)
system.Kubernetes.Should().BeNull();
Expand Down

0 comments on commit a36f868

Please sign in to comment.