Skip to content

Commit

Permalink
[Resources.OperatingSystem] Implement additional osdetector attributes (
Browse files Browse the repository at this point in the history
  • Loading branch information
ysolomchenko authored Aug 30, 2024
1 parent 7fcae49 commit fbca637
Show file tree
Hide file tree
Showing 10 changed files with 371 additions and 13 deletions.
8 changes: 8 additions & 0 deletions src/OpenTelemetry.Resources.OperatingSystem/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
</ItemGroup>

<ItemGroup>
<Compile Include="$(RepoRoot)\src\Shared\ExceptionExtensions.cs" Link="Includes\ExceptionExtensions.cs" />
<Compile Include="$(RepoRoot)\src\Shared\Guard.cs" Link="Includes\Guard.cs" />
</ItemGroup>

Expand Down
236 changes: 228 additions & 8 deletions src/OpenTelemetry.Resources.OperatingSystem/OperatingSystemDetector.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -13,23 +15,98 @@ namespace OpenTelemetry.Resources.OperatingSystem;
/// </summary>
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)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="OperatingSystemDetector"/> class for testing.
/// </summary>
/// <param name="osType">The target platform identifier, specifying the operating system type from SemanticConventions.</param>
/// <param name="registryKey">The string path in the Windows Registry to retrieve specific Windows attributes.</param>
/// <param name="etcOsReleasePath">The string path to the file used to obtain Linux attributes.</param>
/// <param name="plistFilePaths">An array of file paths used to retrieve MacOS attributes from plist files.</param>
internal OperatingSystemDetector(string? osType, string? registryKey, string[]? etcOsReleasePath, string[]? plistFilePaths)
{
this.osType = osType;
this.registryKey = registryKey;
this.etcOsReleasePaths = etcOsReleasePath;
this.plistFilePaths = plistFilePaths;
}

/// <summary>
/// Detects the resource attributes from the operating system.
/// </summary>
/// <returns>Resource with key-value pairs of resource attributes.</returns>
///
public Resource Detect()
{
var osType = GetOSType();

if (osType == null)
var attributes = new List<KeyValuePair<string, object>>(5);
if (this.osType == null)
{
return Resource.Empty;
}

return new Resource(
[
new(AttributeOperatingSystemType, osType),
]);
attributes.Add(new KeyValuePair<string, object>(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<KeyValuePair<string, object>> 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<string, object>(key, value!));
}

private static string? GetOSType()
Expand All @@ -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<KeyValuePair<string, object>> 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<KeyValuePair<string, object>> 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<char> buildId = default, name = default, version = default;

foreach (var line in osReleaseContent)
{
ReadOnlySpan<char> 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<char> line, ReadOnlySpan<char> prefix, ref ReadOnlySpan<char> value)
{
if (!line.StartsWith(prefix))
{
return false;
}

ReadOnlySpan<char> 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<KeyValuePair<string, object>> 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
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
3 changes: 2 additions & 1 deletion src/OpenTelemetry.Resources.OperatingSystem/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,13 @@
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Resources.OperatingSystem\OpenTelemetry.Resources.OperatingSystem.csproj" />
</ItemGroup>

<ItemGroup>
<None Update="Samples\os-release">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Samples\SystemVersion.plist">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
Loading

0 comments on commit fbca637

Please sign in to comment.