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