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

[ResourceDetector.Host] Add host.id for non-containerized systems #1631

Merged
merged 25 commits into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
Loading