diff --git a/src/OpenTelemetry.Resources.OperatingSystem/CHANGELOG.md b/src/OpenTelemetry.Resources.OperatingSystem/CHANGELOG.md
index 24e7e0ba47..be9faebeed 100644
--- a/src/OpenTelemetry.Resources.OperatingSystem/CHANGELOG.md
+++ b/src/OpenTelemetry.Resources.OperatingSystem/CHANGELOG.md
@@ -2,6 +2,14 @@
## Unreleased
+* Implement
+ `os.build_id`,
+ `os.description`,
+ `os.name`,
+ `os.version` attributes in
+ `OpenTelemetry.ResourceDetectors.OperatingSystem`.
+ ([#1983](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/1983))
+
## 0.1.0-alpha.2
Released 2024-Jul-22
diff --git a/src/OpenTelemetry.Resources.OperatingSystem/OpenTelemetry.Resources.OperatingSystem.csproj b/src/OpenTelemetry.Resources.OperatingSystem/OpenTelemetry.Resources.OperatingSystem.csproj
index de79c48fe9..95c55dec8b 100644
--- a/src/OpenTelemetry.Resources.OperatingSystem/OpenTelemetry.Resources.OperatingSystem.csproj
+++ b/src/OpenTelemetry.Resources.OperatingSystem/OpenTelemetry.Resources.OperatingSystem.csproj
@@ -19,6 +19,7 @@
+
diff --git a/src/OpenTelemetry.Resources.OperatingSystem/OperatingSystemDetector.cs b/src/OpenTelemetry.Resources.OperatingSystem/OperatingSystemDetector.cs
index 4bcce208ba..976307fba8 100644
--- a/src/OpenTelemetry.Resources.OperatingSystem/OperatingSystemDetector.cs
+++ b/src/OpenTelemetry.Resources.OperatingSystem/OperatingSystemDetector.cs
@@ -1,9 +1,11 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
-#if !NETFRAMEWORK
+#if NET
using System.Runtime.InteropServices;
+using System.Xml.Linq;
#endif
+
using static OpenTelemetry.Resources.OperatingSystem.OperatingSystemSemanticConventions;
namespace OpenTelemetry.Resources.OperatingSystem;
@@ -13,23 +15,98 @@ namespace OpenTelemetry.Resources.OperatingSystem;
///
internal sealed class OperatingSystemDetector : IResourceDetector
{
+ private const string RegistryKey = @"SOFTWARE\Microsoft\Windows NT\CurrentVersion";
+ private static readonly string[] DefaultEtcOsReleasePaths =
+ [
+ "/etc/os-release",
+ "/usr/lib/os-release"
+ ];
+
+ private static readonly string[] DefaultPlistFilePaths =
+ [
+ "/System/Library/CoreServices/SystemVersion.plist",
+ "/System/Library/CoreServices/ServerVersion.plist"
+ ];
+
+ private readonly string? osType;
+ private readonly string? registryKey;
+ private readonly string[]? etcOsReleasePaths;
+ private readonly string[]? plistFilePaths;
+
+ internal OperatingSystemDetector()
+ : this(
+ GetOSType(),
+ RegistryKey,
+ DefaultEtcOsReleasePaths,
+ DefaultPlistFilePaths)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class for testing.
+ ///
+ /// The target platform identifier, specifying the operating system type from SemanticConventions.
+ /// The string path in the Windows Registry to retrieve specific Windows attributes.
+ /// The string path to the file used to obtain Linux attributes.
+ /// An array of file paths used to retrieve MacOS attributes from plist files.
+ internal OperatingSystemDetector(string? osType, string? registryKey, string[]? etcOsReleasePath, string[]? plistFilePaths)
+ {
+ this.osType = osType;
+ this.registryKey = registryKey;
+ this.etcOsReleasePaths = etcOsReleasePath;
+ this.plistFilePaths = plistFilePaths;
+ }
+
///
/// Detects the resource attributes from the operating system.
///
/// Resource with key-value pairs of resource attributes.
+ ///
public Resource Detect()
{
- var osType = GetOSType();
-
- if (osType == null)
+ var attributes = new List>(5);
+ if (this.osType == null)
{
return Resource.Empty;
}
- return new Resource(
- [
- new(AttributeOperatingSystemType, osType),
- ]);
+ attributes.Add(new KeyValuePair(AttributeOperatingSystemType, this.osType));
+
+ AddAttributeIfNotNullOrEmpty(attributes, AttributeOperatingSystemDescription, GetOSDescription());
+
+ switch (this.osType)
+ {
+ case OperatingSystemsValues.Windows:
+ this.AddWindowsAttributes(attributes);
+ break;
+#if NET
+ case OperatingSystemsValues.Linux:
+ this.AddLinuxAttributes(attributes);
+ break;
+ case OperatingSystemsValues.Darwin:
+ this.AddMacOSAttributes(attributes);
+ break;
+#endif
+ }
+
+ return new Resource(attributes);
+ }
+
+ private static void AddAttributeIfNotNullOrEmpty(List> attributes, string key, object? value)
+ {
+ if (value == null)
+ {
+ OperatingSystemResourcesEventSource.Log.FailedToValidateValue("The provided value is null");
+ return;
+ }
+
+ if (value is string strValue && string.IsNullOrEmpty(strValue))
+ {
+ OperatingSystemResourcesEventSource.Log.FailedToValidateValue("The provided value string is empty.");
+ return;
+ }
+
+ attributes.Add(new KeyValuePair(key, value!));
}
private static string? GetOSType()
@@ -55,4 +132,147 @@ public Resource Detect()
}
#endif
}
+
+ private static string GetOSDescription()
+ {
+#if NET
+ return RuntimeInformation.OSDescription;
+#else
+ return Environment.OSVersion.ToString();
+#endif
+ }
+
+#pragma warning disable CA1416
+ private void AddWindowsAttributes(List> attributes)
+ {
+ try
+ {
+ using var key = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(this.registryKey!);
+ if (key != null)
+ {
+ AddAttributeIfNotNullOrEmpty(attributes, AttributeOperatingSystemBuildId, key.GetValue("CurrentBuildNumber")?.ToString());
+ AddAttributeIfNotNullOrEmpty(attributes, AttributeOperatingSystemName, key.GetValue("ProductName")?.ToString());
+ AddAttributeIfNotNullOrEmpty(attributes, AttributeOperatingSystemVersion, key.GetValue("CurrentVersion")?.ToString());
+ }
+ }
+ catch (Exception ex)
+ {
+ OperatingSystemResourcesEventSource.Log.ResourceAttributesExtractException("Failed to get Windows attributes", ex);
+ }
+ }
+#pragma warning restore CA1416
+
+#if NET
+ // based on:
+ // https://github.com/dotnet/runtime/blob/main/src/libraries/Common/src/Interop/Linux/os-release/Interop.OSReleaseFile.cs
+ private void AddLinuxAttributes(List> attributes)
+ {
+ try
+ {
+ string? etcOsReleasePath = this.etcOsReleasePaths!.FirstOrDefault(File.Exists);
+ if (string.IsNullOrEmpty(etcOsReleasePath))
+ {
+ OperatingSystemResourcesEventSource.Log.FailedToFindFile("Failed to find the os-release file");
+ return;
+ }
+
+ var osReleaseContent = File.ReadAllLines(etcOsReleasePath);
+ ReadOnlySpan buildId = default, name = default, version = default;
+
+ foreach (var line in osReleaseContent)
+ {
+ ReadOnlySpan lineSpan = line.AsSpan();
+
+ _ = TryGetFieldValue(lineSpan, "BUILD_ID=", ref buildId) ||
+ TryGetFieldValue(lineSpan, "NAME=", ref name) ||
+ TryGetFieldValue(lineSpan, "VERSION_ID=", ref version);
+ }
+
+ // TODO: fallback for buildId
+
+ AddAttributeIfNotNullOrEmpty(attributes, AttributeOperatingSystemBuildId, buildId.IsEmpty ? null : buildId.ToString());
+ AddAttributeIfNotNullOrEmpty(attributes, AttributeOperatingSystemName, name.IsEmpty ? "Linux" : name.ToString());
+ AddAttributeIfNotNullOrEmpty(attributes, AttributeOperatingSystemVersion, version.IsEmpty ? null : version.ToString());
+ }
+ catch (Exception ex)
+ {
+ OperatingSystemResourcesEventSource.Log.ResourceAttributesExtractException("Failed to get Linux attributes", ex);
+ }
+
+ static bool TryGetFieldValue(ReadOnlySpan line, ReadOnlySpan prefix, ref ReadOnlySpan value)
+ {
+ if (!line.StartsWith(prefix))
+ {
+ return false;
+ }
+
+ ReadOnlySpan fieldValue = line.Slice(prefix.Length);
+
+ // Remove enclosing quotes if present.
+ if (fieldValue.Length >= 2 &&
+ (fieldValue[0] == '"' || fieldValue[0] == '\'') &&
+ fieldValue[0] == fieldValue[^1])
+ {
+ fieldValue = fieldValue[1..^1];
+ }
+
+ value = fieldValue;
+ return true;
+ }
+ }
+
+ private void AddMacOSAttributes(List> attributes)
+ {
+ try
+ {
+ string? plistFilePath = this.plistFilePaths!.FirstOrDefault(File.Exists);
+ if (string.IsNullOrEmpty(plistFilePath))
+ {
+ OperatingSystemResourcesEventSource.Log.FailedToFindFile("No suitable plist file found");
+ return;
+ }
+
+ XDocument doc = XDocument.Load(plistFilePath);
+ var dict = doc.Root?.Element("dict");
+
+ string? buildId = null, name = null, version = null;
+
+ if (dict != null)
+ {
+ var keys = dict.Elements("key").ToList();
+ var values = dict.Elements("string").ToList();
+
+ if (keys.Count != values.Count)
+ {
+ OperatingSystemResourcesEventSource.Log.FailedToValidateValue($"Failed to get MacOS attributes: The number of keys does not match the number of values. Keys count: {keys.Count}, Values count: {values.Count}");
+ return;
+ }
+
+ for (int i = 0; i < keys.Count; i++)
+ {
+ switch (keys[i].Value)
+ {
+ case "ProductBuildVersion":
+ buildId = values[i].Value;
+ break;
+ case "ProductName":
+ name = values[i].Value;
+ break;
+ case "ProductVersion":
+ version = values[i].Value;
+ break;
+ }
+ }
+ }
+
+ AddAttributeIfNotNullOrEmpty(attributes, AttributeOperatingSystemBuildId, buildId);
+ AddAttributeIfNotNullOrEmpty(attributes, AttributeOperatingSystemName, name);
+ AddAttributeIfNotNullOrEmpty(attributes, AttributeOperatingSystemVersion, version);
+ }
+ catch (Exception ex)
+ {
+ OperatingSystemResourcesEventSource.Log.ResourceAttributesExtractException("Failed to get MacOS attributes", ex);
+ }
+ }
+#endif
}
diff --git a/src/OpenTelemetry.Resources.OperatingSystem/OperatingSystemResourcesEventSource.cs b/src/OpenTelemetry.Resources.OperatingSystem/OperatingSystemResourcesEventSource.cs
new file mode 100644
index 0000000000..12b82ec7aa
--- /dev/null
+++ b/src/OpenTelemetry.Resources.OperatingSystem/OperatingSystemResourcesEventSource.cs
@@ -0,0 +1,44 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Diagnostics.Tracing;
+using OpenTelemetry.Internal;
+
+namespace OpenTelemetry.Resources.OperatingSystem;
+
+[EventSource(Name = "OpenTelemetry-Resources-OperatingSystem")]
+internal sealed class OperatingSystemResourcesEventSource : EventSource
+{
+ public static OperatingSystemResourcesEventSource Log = new();
+
+ private const int EventIdFailedToExtractAttributes = 1;
+ private const int EventIdFailedToValidateValue = 2;
+ private const int EventIdFailedToFindFile = 3;
+
+ [NonEvent]
+ public void ResourceAttributesExtractException(string format, Exception ex)
+ {
+ if (this.IsEnabled(EventLevel.Warning, EventKeywords.All))
+ {
+ this.FailedToExtractResourceAttributes(format, ex.ToInvariantString());
+ }
+ }
+
+ [Event(EventIdFailedToExtractAttributes, Message = "Failed to extract resource attributes in '{0}'.", Level = EventLevel.Warning)]
+ public void FailedToExtractResourceAttributes(string format, string exception)
+ {
+ this.WriteEvent(EventIdFailedToExtractAttributes, format, exception);
+ }
+
+ [Event(EventIdFailedToValidateValue, Message = "Failed to validate value. Details: '{0}'", Level = EventLevel.Warning)]
+ public void FailedToValidateValue(string error)
+ {
+ this.WriteEvent(EventIdFailedToValidateValue, error);
+ }
+
+ [Event(EventIdFailedToFindFile, Message = "Process timeout occurred: '{0}'", Level = EventLevel.Warning)]
+ public void FailedToFindFile(string error)
+ {
+ this.WriteEvent(EventIdFailedToFindFile, error);
+ }
+}
diff --git a/src/OpenTelemetry.Resources.OperatingSystem/OperatingSystemSemanticConventions.cs b/src/OpenTelemetry.Resources.OperatingSystem/OperatingSystemSemanticConventions.cs
index 356ad734da..87d8c54081 100644
--- a/src/OpenTelemetry.Resources.OperatingSystem/OperatingSystemSemanticConventions.cs
+++ b/src/OpenTelemetry.Resources.OperatingSystem/OperatingSystemSemanticConventions.cs
@@ -6,6 +6,10 @@ namespace OpenTelemetry.Resources.OperatingSystem;
internal static class OperatingSystemSemanticConventions
{
public const string AttributeOperatingSystemType = "os.type";
+ public const string AttributeOperatingSystemBuildId = "os.build_id";
+ public const string AttributeOperatingSystemDescription = "os.description";
+ public const string AttributeOperatingSystemName = "os.name";
+ public const string AttributeOperatingSystemVersion = "os.version";
public static class OperatingSystemsValues
{
diff --git a/src/OpenTelemetry.Resources.OperatingSystem/README.md b/src/OpenTelemetry.Resources.OperatingSystem/README.md
index 7b74713a40..40c1c5a9b1 100644
--- a/src/OpenTelemetry.Resources.OperatingSystem/README.md
+++ b/src/OpenTelemetry.Resources.OperatingSystem/README.md
@@ -54,7 +54,8 @@ using var loggerFactory = LoggerFactory.Create(builder =>
The resource detectors will record the following metadata based on where
your application is running:
-- **OperatingSystemDetector**: `os.type`.
+- **OperatingSystemDetector**: `os.type`, `os.build_id`, `os.description`,
+ `os.name`, `os.version`.
## References
diff --git a/test/OpenTelemetry.Resources.OperatingSystem.Tests/OpenTelemetry.Resources.OperatingSystem.Tests.csproj b/test/OpenTelemetry.Resources.OperatingSystem.Tests/OpenTelemetry.Resources.OperatingSystem.Tests.csproj
index c5b25f22ea..b8c188d1d8 100644
--- a/test/OpenTelemetry.Resources.OperatingSystem.Tests/OpenTelemetry.Resources.OperatingSystem.Tests.csproj
+++ b/test/OpenTelemetry.Resources.OperatingSystem.Tests/OpenTelemetry.Resources.OperatingSystem.Tests.csproj
@@ -11,4 +11,13 @@
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+
diff --git a/test/OpenTelemetry.Resources.OperatingSystem.Tests/OperatingSystemDetectorTests.cs b/test/OpenTelemetry.Resources.OperatingSystem.Tests/OperatingSystemDetectorTests.cs
index dbcf1bb113..68ae79e3c5 100644
--- a/test/OpenTelemetry.Resources.OperatingSystem.Tests/OperatingSystemDetectorTests.cs
+++ b/test/OpenTelemetry.Resources.OperatingSystem.Tests/OperatingSystemDetectorTests.cs
@@ -12,32 +12,77 @@ public class OperatingSystemDetectorTests
public void TestOperatingSystemAttributes()
{
var resource = ResourceBuilder.CreateEmpty().AddOperatingSystemDetector().Build();
-
var resourceAttributes = resource.Attributes.ToDictionary(x => x.Key, x => (string)x.Value);
string expectedPlatform;
+ string expectedDescription;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
expectedPlatform = OperatingSystemSemanticConventions.OperatingSystemsValues.Windows;
+ expectedDescription = "Windows";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
expectedPlatform = OperatingSystemSemanticConventions.OperatingSystemsValues.Linux;
+ expectedDescription = "Linux";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
expectedPlatform = OperatingSystemSemanticConventions.OperatingSystemsValues.Darwin;
+ expectedDescription = "Darwin";
}
else
{
throw new PlatformNotSupportedException("Unknown platform");
}
- Assert.Single(resourceAttributes);
+ Assert.Contains(OperatingSystemSemanticConventions.AttributeOperatingSystemDescription, resourceAttributes.Keys);
+ Assert.Contains(OperatingSystemSemanticConventions.AttributeOperatingSystemName, resourceAttributes.Keys);
+ Assert.Contains(OperatingSystemSemanticConventions.AttributeOperatingSystemType, resourceAttributes.Keys);
+ Assert.Contains(OperatingSystemSemanticConventions.AttributeOperatingSystemVersion, resourceAttributes.Keys);
+
+ // Not checking on Linux because the description may vary depending on the distribution.
+ if (expectedDescription != "Linux")
+ {
+ Assert.Contains(OperatingSystemSemanticConventions.AttributeOperatingSystemBuildId, resourceAttributes.Keys);
+ Assert.Contains(expectedDescription, resourceAttributes[OperatingSystemSemanticConventions.AttributeOperatingSystemDescription]);
+ Assert.Equal(5, resourceAttributes.Count);
+ }
- Assert.True(resourceAttributes.ContainsKey(OperatingSystemSemanticConventions.AttributeOperatingSystemType));
+ Assert.Equal(expectedPlatform, resourceAttributes[OperatingSystemSemanticConventions.AttributeOperatingSystemType]);
+ }
+
+#if NET
+ [Fact]
+ public void TestParseMacOSPlist()
+ {
+ string path = "Samples/SystemVersion.plist";
+ var osDetector = new OperatingSystemDetector(
+ OperatingSystemSemanticConventions.OperatingSystemsValues.Darwin,
+ null,
+ null,
+ [path]);
+ var attributes = osDetector.Detect().Attributes.ToDictionary(x => x.Key, x => (string)x.Value);
+
+ Assert.Equal("Mac OS X", attributes[OperatingSystemSemanticConventions.AttributeOperatingSystemName]);
+ Assert.Equal("10.6.8", attributes[OperatingSystemSemanticConventions.AttributeOperatingSystemVersion]);
+ Assert.Equal("10K549", attributes[OperatingSystemSemanticConventions.AttributeOperatingSystemBuildId]);
+ }
+
+ [Fact]
+ public void TestParseLinuxOsRelease()
+ {
+ string path = "Samples/os-release";
+ var osDetector = new OperatingSystemDetector(
+ OperatingSystemSemanticConventions.OperatingSystemsValues.Linux,
+ null,
+ [path],
+ null);
+ var attributes = osDetector.Detect().Attributes.ToDictionary(x => x.Key, x => (string)x.Value);
- Assert.Equal(resourceAttributes[OperatingSystemSemanticConventions.AttributeOperatingSystemType], expectedPlatform);
+ Assert.Equal("Ubuntu", attributes[OperatingSystemSemanticConventions.AttributeOperatingSystemName]);
+ Assert.Equal("22.04", attributes[OperatingSystemSemanticConventions.AttributeOperatingSystemVersion]);
}
+#endif
}
diff --git a/test/OpenTelemetry.Resources.OperatingSystem.Tests/Samples/SystemVersion.plist b/test/OpenTelemetry.Resources.OperatingSystem.Tests/Samples/SystemVersion.plist
new file mode 100644
index 0000000000..ef45504dcd
--- /dev/null
+++ b/test/OpenTelemetry.Resources.OperatingSystem.Tests/Samples/SystemVersion.plist
@@ -0,0 +1,16 @@
+
+
+
+
+ProductBuildVersion
+10K549
+ProductCopyright
+1983-2011 Apple Inc.
+ProductName
+Mac OS X
+ProductUserVisibleVersion
+10.6.8
+ProductVersion
+10.6.8
+
+
diff --git a/test/OpenTelemetry.Resources.OperatingSystem.Tests/Samples/os-release b/test/OpenTelemetry.Resources.OperatingSystem.Tests/Samples/os-release
new file mode 100644
index 0000000000..af5f81109a
--- /dev/null
+++ b/test/OpenTelemetry.Resources.OperatingSystem.Tests/Samples/os-release
@@ -0,0 +1,10 @@
+NAME=Ubuntu
+VERSION="22.04 LTS (Jammy Jellyfish)"
+VERSION_ID="22.04"
+VERSION_CODENAME=jammy
+ID=ubuntu
+HOME_URL=https://www.ubuntu.com/
+SUPPORT_URL=https://help.ubuntu.com/
+BUG_REPORT_URL=https://bugs.launchpad.net/ubuntu
+PRIVACY_POLICY_URL=https://www.ubuntu.com/legal/terms-and-policies/privacy-policy
+UBUNTU_CODENAME=jammy