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

update-json-specs.yml: add container_metadata_discovery.json #2156

Merged
merged 9 commits into from
Jun 5, 2024
63 changes: 58 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,28 @@ 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 +66,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 +198,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 +210,34 @@ 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 +250,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;
}
}

This file was deleted.

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
Loading