Skip to content

Commit

Permalink
[ResourceDetector.Host] Add host.id for non-containerized systems (#1631
Browse files Browse the repository at this point in the history
)
  • Loading branch information
matt-hensley authored Apr 12, 2024
1 parent b2d54af commit 44c8576
Show file tree
Hide file tree
Showing 8 changed files with 283 additions and 4 deletions.
4 changes: 4 additions & 0 deletions src/OpenTelemetry.ResourceDetectors.Host/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

* Adds support for `host.id` resource attribute on non-containerized systems.
`host.id` will be set per [semantic convention rules](https://github.com/open-telemetry/semantic-conventions/blob/v1.24.0/docs/resource/host.md)
([#1631](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/1631))

## 0.1.0-alpha.3

Released 2024-Apr-05
Expand Down
164 changes: 162 additions & 2 deletions src/OpenTelemetry.ResourceDetectors.Host/HostDetector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using Microsoft.Win32;
using OpenTelemetry.Resources;

namespace OpenTelemetry.ResourceDetectors.Host;
Expand All @@ -12,6 +16,40 @@ namespace OpenTelemetry.ResourceDetectors.Host;
/// </summary>
public sealed class HostDetector : IResourceDetector
{
private const string ETCMACHINEID = "/etc/machine-id";
private const string ETCVARDBUSMACHINEID = "/var/lib/dbus/machine-id";
private readonly PlatformID platformId;
private readonly Func<IEnumerable<string>> getFilePaths;
private readonly Func<string?> getMacOsMachineId;
private readonly Func<string?> getWindowsMachineId;

/// <summary>
/// Initializes a new instance of the <see cref="HostDetector"/> class.
/// </summary>
public HostDetector()
: this(
Environment.OSVersion.Platform,
GetFilePaths,
GetMachineIdMacOs,
GetMachineIdWindows)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="HostDetector"/> class for testing.
/// </summary>
/// <param name="platformId">Target platform ID.</param>
/// <param name="getFilePaths">Function to get Linux file paths to probe.</param>
/// <param name="getMacOsMachineId">Function to get MacOS machine ID.</param>
/// <param name="getWindowsMachineId">Function to get Windows machine ID.</param>
internal HostDetector(PlatformID platformId, Func<IEnumerable<string>> getFilePaths, Func<string?> getMacOsMachineId, Func<string?> getWindowsMachineId)
{
this.platformId = platformId;
this.getFilePaths = getFilePaths ?? throw new ArgumentNullException(nameof(getFilePaths));
this.getMacOsMachineId = getMacOsMachineId ?? throw new ArgumentNullException(nameof(getMacOsMachineId));
this.getWindowsMachineId = getWindowsMachineId ?? throw new ArgumentNullException(nameof(getWindowsMachineId));
}

/// <summary>
/// Detects the resource attributes from host.
/// </summary>
Expand All @@ -20,10 +58,18 @@ public Resource Detect()
{
try
{
return new Resource(new List<KeyValuePair<string, object>>(1)
var attributes = new List<KeyValuePair<string, object>>(2)
{
new(HostSemanticConventions.AttributeHostName, Environment.MachineName),
});
};
var machineId = this.GetMachineId();

if (machineId != null && !string.IsNullOrEmpty(machineId))
{
attributes.Add(new(HostSemanticConventions.AttributeHostId, machineId));
}

return new Resource(attributes);
}
catch (InvalidOperationException ex)
{
Expand All @@ -33,4 +79,118 @@ public Resource Detect()

return Resource.Empty;
}

internal static string? ParseMacOsOutput(string? output)
{
if (output == null || string.IsNullOrEmpty(output))
{
return null;
}

var lines = output.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);

foreach (var line in lines)
{
#if NETFRAMEWORK
if (line.IndexOf("IOPlatformUUID", StringComparison.OrdinalIgnoreCase) >= 0)
#else
if (line.Contains("IOPlatformUUID", StringComparison.OrdinalIgnoreCase))
#endif
{
var parts = line.Split('"');

if (parts.Length > 3)
{
return parts[3];
}
}
}

return null;
}

private static IEnumerable<string> GetFilePaths()
{
yield return ETCMACHINEID;
yield return ETCVARDBUSMACHINEID;
}

private static string? GetMachineIdMacOs()
{
try
{
var startInfo = new ProcessStartInfo
{
FileName = "sh",
Arguments = "ioreg -rd1 -c IOPlatformExpertDevice",
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
};

var sb = new StringBuilder();
using var process = Process.Start(startInfo);
process?.WaitForExit();
sb.Append(process?.StandardOutput.ReadToEnd());
return sb.ToString();
}
catch (Exception ex)
{
HostResourceEventSource.Log.ResourceAttributesExtractException(nameof(HostDetector), ex);
}

return null;
}

#pragma warning disable CA1416
// stylecop wants this protected by System.OperatingSystem.IsWindows
// this type only exists in .NET 5+
private static string? GetMachineIdWindows()
{
try
{
using var subKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Cryptography", false);
return subKey?.GetValue("MachineGuid") as string ?? null;
}
catch (Exception ex)
{
HostResourceEventSource.Log.ResourceAttributesExtractException(nameof(HostDetector), ex);
}

return null;
}
#pragma warning restore CA1416

private string? GetMachineId()
{
return this.platformId switch
{
PlatformID.Unix => this.GetMachineIdLinux(),
PlatformID.MacOSX => ParseMacOsOutput(this.getMacOsMachineId()),
PlatformID.Win32NT => this.getWindowsMachineId(),
_ => null,
};
}

private string? GetMachineIdLinux()
{
var paths = this.getFilePaths();

foreach (var path in paths)
{
if (File.Exists(path))
{
try
{
return File.ReadAllText(path).Trim();
}
catch (Exception ex)
{
HostResourceEventSource.Log.ResourceAttributesExtractException(nameof(HostDetector), ex);
}
}
}

return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ namespace OpenTelemetry.ResourceDetectors.Host;
internal static class HostSemanticConventions
{
public const string AttributeHostName = "host.name";
public const string AttributeHostId = "host.id";
}
2 changes: 1 addition & 1 deletion src/OpenTelemetry.ResourceDetectors.Host/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ var tracerProvider = Sdk.CreateTracerProviderBuilder()
The resource detectors will record the following metadata based on where
your application is running:

- **HostDetector**: `host.name`.
- **HostDetector**: `host.id` (when running on non-containerized systems), `host.name`.

## References

Expand Down
105 changes: 104 additions & 1 deletion test/OpenTelemetry.ResourceDetectors.Host.Tests/HostDetectorTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

using System;
using System.Collections.Generic;
using System.Linq;
using OpenTelemetry.Resources;
using Xunit;
Expand All @@ -9,15 +11,116 @@ namespace OpenTelemetry.ResourceDetectors.Host.Tests;

public class HostDetectorTests
{
private const string MacOSMachineIdOutput = @"+-o J293AP <class IOPlatformExpertDevice, id 0x100000227, registered, matched,$
{
""IOPolledInterface"" = ""AppleARMWatchdogTimerHibernateHandler is not seria$
""#address-cells"" = <02000000>
""AAPL,phandle"" = <01000000>
""serial-number"" = <432123465233514651303544000000000000000000000000000000$
""IOBusyInterest"" = ""IOCommand is not serializable""
""target-type"" = <""J293"">
""platform-name"" = <743831303300000000000000000000000000000000000000000000$
""secure-root-prefix"" = <""md"">
""name"" = <""device-tree"">
""region-info"" = <4c4c2f41000000000000000000000000000000000000000000000000$
""manufacturer"" = <""Apple Inc."">
""compatible"" = <""J293AP"",""MacBookPro17,1"",""AppleARM"">
""config-number"" = <000000000000000000000000000000000000000000000000000000$
""IOPlatformSerialNumber"" = ""A01BC3QFQ05D""
""regulatory-model-number"" = <41323333380000000000000000000000000000000000$
""time-stamp"" = <""Mon Jun 27 20:12:10 PDT 2022"">
""clock-frequency"" = <00366e01>
""model"" = <""MacBookPro17,1"">
""mlb-serial-number"" = <432123413230363030455151384c4c314a0000000000000000$
""model-number"" = <4d59443832000000000000000000000000000000000000000000000$
""IONWInterrupts"" = ""IONWInterrupts""
""model-config"" = <""SUNWAY;MoPED=0x803914B08BE6C5AF0E6C990D7D8240DA4CAC2FF$
""device_type"" = <""bootrom"">
""#size-cells"" = <02000000>
""IOPlatformUUID"" = ""1AB2345C-03E4-57D4-A375-1234D48DE123""
}";

private static readonly IEnumerable<string> ETCMACHINEID = new[] { "Samples/etc_machineid" };
private static readonly IEnumerable<string> ETCVARDBUSMACHINEID = new[] { "Samples/etc_var_dbus_machineid" };

[Fact]
public void TestHostAttributes()
{
var resource = ResourceBuilder.CreateEmpty().AddDetector(new HostDetector()).Build();

var resourceAttributes = resource.Attributes.ToDictionary(x => x.Key, x => (string)x.Value);

Assert.Single(resourceAttributes);
Assert.Equal(2, resourceAttributes.Count);

Assert.NotEmpty(resourceAttributes[HostSemanticConventions.AttributeHostName]);
Assert.NotEmpty(resourceAttributes[HostSemanticConventions.AttributeHostId]);
}

[Fact]
public void TestHostMachineIdLinux()
{
var combos = new[]
{
(Enumerable.Empty<string>(), null),
(ETCMACHINEID, "etc_machineid"),
(ETCVARDBUSMACHINEID, "etc_var_dbus_machineid"),
(Enumerable.Concat(ETCMACHINEID, ETCVARDBUSMACHINEID), "etc_machineid"),
};

foreach (var (path, expected) in combos)
{
var detector = new HostDetector(
PlatformID.Unix,
() => path,
() => throw new Exception("should not be called"),
() => throw new Exception("should not be called"));
var resource = ResourceBuilder.CreateEmpty().AddDetector(detector).Build();
var resourceAttributes = resource.Attributes.ToDictionary(x => x.Key, x => (string)x.Value);

if (string.IsNullOrEmpty(expected))
{
Assert.False(resourceAttributes.ContainsKey(HostSemanticConventions.AttributeHostId));
}
else
{
Assert.NotEmpty(resourceAttributes[HostSemanticConventions.AttributeHostId]);
Assert.Equal(expected, resourceAttributes[HostSemanticConventions.AttributeHostId]);
}
}
}

[Fact]
public void TestHostMachineIdMacOs()
{
var detector = new HostDetector(
PlatformID.MacOSX,
() => Enumerable.Empty<string>(),
() => MacOSMachineIdOutput,
() => throw new Exception("should not be called"));
var resource = ResourceBuilder.CreateEmpty().AddDetector(detector).Build();
var resourceAttributes = resource.Attributes.ToDictionary(x => x.Key, x => (string)x.Value);
Assert.NotEmpty(resourceAttributes[HostSemanticConventions.AttributeHostId]);
Assert.Equal("1AB2345C-03E4-57D4-A375-1234D48DE123", resourceAttributes[HostSemanticConventions.AttributeHostId]);
}

[Fact]
public void TestParseMacOsOutput()
{
var id = HostDetector.ParseMacOsOutput(MacOSMachineIdOutput);
Assert.Equal("1AB2345C-03E4-57D4-A375-1234D48DE123", id);
}

[Fact]
public void TestHostMachineIdWindows()
{
var detector = new HostDetector(
PlatformID.Win32NT,
() => Enumerable.Empty<string>(),
() => throw new Exception("should not be called"),
() => "windows-machine-id");
var resource = ResourceBuilder.CreateEmpty().AddDetector(detector).Build();
var resourceAttributes = resource.Attributes.ToDictionary(x => x.Key, x => (string)x.Value);
Assert.NotEmpty(resourceAttributes[HostSemanticConventions.AttributeHostId]);
Assert.Equal("windows-machine-id", resourceAttributes[HostSemanticConventions.AttributeHostId]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,13 @@
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.ResourceDetectors.Host\OpenTelemetry.ResourceDetectors.Host.csproj" />
</ItemGroup>

<ItemGroup>
<None Update="Samples\etc_machineid">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Samples\etc_var_dbus_machineid">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
etc_machineid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
etc_var_dbus_machineid

0 comments on commit 44c8576

Please sign in to comment.