Skip to content

Commit

Permalink
Enhance resource attributes (#96)
Browse files Browse the repository at this point in the history
* Enhance resource attributes for better interop with Elastic APM

Sets `service.instance.id`, `host.id` and `host.name`.

* Add tests

* Fix license header

* Fix formatting
  • Loading branch information
stevejgordon authored Jun 5, 2024
1 parent 7278a5a commit 511448e
Show file tree
Hide file tree
Showing 7 changed files with 426 additions and 25 deletions.
5 changes: 2 additions & 3 deletions src/Elastic.OpenTelemetry/Elastic.OpenTelemetry.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net6.0;net462</TargetFrameworks>
<TargetFrameworks>netstandard2.0;netstandard2.1;net462;net6.0;net8.0</TargetFrameworks>
<Title>Elastic OpenTelemetry .NET Distribution</Title>
<Description>OpenTelemetry extensions for Elastic Observability, fully native with zero code changes</Description>
<Description>OpenTelemetry extensions for Elastic Observability, fully native with zero code changes.</Description>
<PackageTags>elastic;opentelemetry;observabillity;apm;logs;metrics;traces;monitoring</PackageTags>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
Expand All @@ -20,7 +20,6 @@
<ItemGroup>
<PackageReference Include="OpenTelemetry" Version="1.8.1" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.8.1" />

<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.8.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.8.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.GrpcNetClient" Version="1.8.0-beta.1" />
Expand Down
45 changes: 26 additions & 19 deletions src/Elastic.OpenTelemetry/ElasticOpenTelemetryBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,40 +81,47 @@ public ElasticOpenTelemetryBuilder(ElasticOpenTelemetryBuilderOptions options)
var openTelemetry =
Microsoft.Extensions.DependencyInjection.OpenTelemetryServicesExtensions.AddOpenTelemetry(Services);

// We always add this so we can identify a distro is being used, even if all Elastic defaults are disabled.
openTelemetry.ConfigureResource(r => r.AddDistroAttributes());

if (options.DistroOptions.EnabledDefaults.Equals(ElasticOpenTelemetryOptions.EnabledElasticDefaults.None))
{
Logger.LogNoElasticDefaults();

// We always add the distro attribute so that we can identify a distro is being used, even if all Elastic defaults are disabled.
openTelemetry.ConfigureResource(r => r.AddDistroAttributes());
return;
}

openTelemetry.ConfigureResource(r => r.AddElasticResourceDefaults(Logger));

//https://github.com/open-telemetry/opentelemetry-dotnet/pull/5400
if (!options.DistroOptions.SkipOtlpExporter)
openTelemetry.UseOtlpExporter();

if (options.DistroOptions.EnabledDefaults.HasFlag(ElasticOpenTelemetryOptions.EnabledElasticDefaults.Logging))
{
//TODO Move to WithLogging once it gets stable
// TODO: Move to WithLogging once it gets stable.
Services.Configure<OpenTelemetryLoggerOptions>(logging =>
{
logging.IncludeFormattedMessage = true;
logging.IncludeScopes = true;
//TODO add processor that adds service.id
});

// Note: We use this log method for now as the WithLogging method is not yet stable.
Logger.LogConfiguredSignalProvider("logging", nameof(OpenTelemetryLoggerOptions));
}

if (options.DistroOptions.EnabledDefaults.HasFlag(ElasticOpenTelemetryOptions.EnabledElasticDefaults.Tracing))
{
openTelemetry.WithTracing(tracing =>
{
tracing
.AddHttpClientInstrumentation()
.AddGrpcClientInstrumentation()
.AddEntityFrameworkCoreInstrumentation();
{
tracing
.AddHttpClientInstrumentation()
.AddGrpcClientInstrumentation()
.AddEntityFrameworkCoreInstrumentation();
tracing.AddElasticProcessors(Logger);
tracing.AddElasticProcessors(Logger);
});

Logger.LogConfiguredTracerProvider();
});
Logger.LogConfiguredSignalProvider("tracing", nameof(TracerProviderBuilder));
}

if (options.DistroOptions.EnabledDefaults.HasFlag(ElasticOpenTelemetryOptions.EnabledElasticDefaults.Metrics))
Expand All @@ -126,20 +133,20 @@ public ElasticOpenTelemetryBuilder(ElasticOpenTelemetryBuilderOptions options)
.AddRuntimeInstrumentation()
.AddHttpClientInstrumentation();
Logger.LogConfiguredMeterProvider();
Logger.LogConfiguredSignalProvider("metrics", nameof(MeterProviderBuilder));
});
}
}
}

internal static partial class LoggerMessages
{
[LoggerMessage(EventId = 0, Level = LogLevel.Trace, Message = "ElasticOpenTelemetryBuilder initialized{newline}{StackTrace}.")]
[LoggerMessage(EventId = 0, Level = LogLevel.Information, Message = "ElasticOpenTelemetryBuilder initialized{newline}{StackTrace}.")]
public static partial void LogElasticOpenTelemetryBuilderInitialized(this ILogger logger, string newline, StackTrace stackTrace);

[LoggerMessage(EventId = 1, Level = LogLevel.Trace, Message = "ElasticOpenTelemetryBuilder configured tracing services via the TracerProvider.")]
public static partial void LogConfiguredTracerProvider(this ILogger logger);
[LoggerMessage(EventId = 1, Level = LogLevel.Trace, Message = "ElasticOpenTelemetryBuilder configured {Signal} via the {Provider}.")]
public static partial void LogConfiguredSignalProvider(this ILogger logger, string signal, string provider);

[LoggerMessage(EventId = 2, Level = LogLevel.Trace, Message = "ElasticOpenTelemetryBuilder configured metric services via the MeterProvider.")]
public static partial void LogConfiguredMeterProvider(this ILogger logger);
[LoggerMessage(EventId = 2, Level = LogLevel.Information, Message = "No Elastic defaults were enabled.")]
public static partial void LogNoElasticDefaults(this ILogger logger);
}
72 changes: 70 additions & 2 deletions src/Elastic.OpenTelemetry/Extensions/ResourceBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,82 @@
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information
using Elastic.OpenTelemetry.SemanticConventions;

using System.Diagnostics;
using Elastic.OpenTelemetry.SemanticConventions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using OpenTelemetry.ResourceDetectors.Host;
using OpenTelemetry.Resources;

namespace Elastic.OpenTelemetry.Extensions;

internal static class ResourceBuilderExtensions
/// <summary>
/// Extension methods for <see cref="ResourceBuilder"/>.
/// </summary>
public static class ResourceBuilderExtensions
{
private static readonly string InstanceId = Guid.NewGuid().ToString();

/// <summary>
/// For advanced scenarios, where the all Elastic defaults are not disabled (essentially using the "vanilla" OpenTelemetry SDK),
/// this method can be used to add Elastic resource defaults to the <see cref="ResourceBuilder"/>.
/// </summary>
/// <remarks>
/// After clearing the <see cref="ResourceBuilder"/> the following are added in order:
/// <list type="bullet">
/// <item>A default, fallback <c>service.name</c>.</item>
/// <item>A default, unqiue <c>service.instance.id</c>.</item>
/// <item>The telemetry SDK attributes via <c>AddTelemetrySdk()</c>.</item>
/// <item>The Elastic telemetry distro attributes.</item>
/// <item>Adds resource attributes parsed from OTEL_RESOURCE_ATTRIBUTES, OTEL_SERVICE_NAME environment variables
/// via <c>AddEnvironmentVariableDetector()</c>.</item>
/// <item>Host attributes, <c>host.name</c> and <c>host.id</c> (on supported targets).</item>
/// </list>
/// <para>These mostly mirror what the vanilla SDK does, but allow us to ensure that certain resources attributes that the
/// Elastic APM backend requires to drive the UIs are present in some form. Any of these may be overridden by further
/// resource configuration.</para>
/// </remarks>
/// <param name="builder">A <see cref="ResourceBuilder"/> that will be configured with Elastic defaults.</param>
/// <returns>The <see cref="ResourceBuilder"/> for chaining calls.</returns>
public static ResourceBuilder AddElasticResourceDefaults(this ResourceBuilder builder) =>
builder.AddElasticResourceDefaults(NullLogger.Instance);

internal static ResourceBuilder AddElasticResourceDefaults(this ResourceBuilder builder, ILogger logger)
{
var defaultServiceName = "unknown_service";

try
{
var processName = Process.GetCurrentProcess().ProcessName;
if (!string.IsNullOrWhiteSpace(processName))
{
defaultServiceName = $"{defaultServiceName}:{processName}";
}
}
catch
{
// GetCurrentProcess can throw PlatformNotSupportedException
}

builder
.Clear()
.AddAttributes(new Dictionary<string, object>
{
{ ResourceSemanticConventions.AttributeServiceName, defaultServiceName },
{ ResourceSemanticConventions.AttributeServiceInstanceId, InstanceId }
})
.AddTelemetrySdk()
.AddDistroAttributes()
.AddEnvironmentVariableDetector();

#if NET462_OR_GREATER || NET6_0_OR_GREATER
builder.AddDetector(new HostDetector(logger));
#endif

return builder;
}

internal static ResourceBuilder AddDistroAttributes(this ResourceBuilder builder) =>
builder.AddAttributes(new Dictionary<string, object>
{
Expand Down
193 changes: 193 additions & 0 deletions src/Elastic.OpenTelemetry/Resources/HostDetector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

// Modified from https://github.com/open-telemetry/opentelemetry-dotnet-contrib/blob/44c8576ef1290d9fbb6fbbdc973ae1b344afb4c2/src/OpenTelemetry.ResourceDetectors.Host/HostDetector.cs
// As the host.id is support not yet released, we are using the code from the contrib project directly for now.

// TODO - Switch to the contrib package once the features we need are released.

using System.Diagnostics;
using System.Text;
using Elastic.OpenTelemetry.SemanticConventions;
using Microsoft.Extensions.Logging;
using OpenTelemetry.Resources;
#if NETFRAMEWORK || NET6_0_OR_GREATER
using Microsoft.Win32;
#endif

namespace OpenTelemetry.ResourceDetectors.Host;

/// <summary>
/// Host detector.
/// </summary>
internal 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;

private readonly ILogger _logger;

/// <summary>
/// Initializes a new instance of the <see cref="HostDetector"/> class.
/// </summary>
internal HostDetector(ILogger logger)
{
_platformId = Environment.OSVersion.Platform;
_getFilePaths = GetFilePaths;
_getMacOsMachineId = GetMachineIdMacOs;
_getWindowsMachineId = GetMachineIdWindows;
_logger = logger;
}

/// <summary>
/// Detects the resource attributes from host.
/// </summary>
/// <returns>Resource with key-value pairs of resource attributes.</returns>
public Resource Detect()
{
try
{
var attributes = new List<KeyValuePair<string, object>>(2)
{
new(ResourceSemanticConventions.AttributeHostName, Environment.MachineName),
};
var machineId = GetMachineId();

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

return new Resource(attributes);
}
catch (InvalidOperationException ex)
{
// Handling InvalidOperationException due to https://learn.microsoft.com/en-us/dotnet/api/system.environment.machinename#exceptions
_logger.LogError("Failed to detect host resource due to {Exception}", ex);
}

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 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)
{
_logger.LogError("Failed to get machine ID on MacOS due to {Exception}", ex);
}

return null;
}

#pragma warning disable CA1416
// stylecop wants this protected by System.OperatingSystem.IsWindows
// this type only exists in .NET 5+
private string? GetMachineIdWindows()
{
#if NETFRAMEWORK || NET6_0_OR_GREATER
try
{
using var subKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Cryptography", false);
return subKey?.GetValue("MachineGuid") as string ?? null;
}
catch (Exception ex)
{
_logger.LogError("Failed to get machine ID on Windows due to {Exception}", ex);
}
#endif

return null;
}
#pragma warning restore CA1416

private string? GetMachineId() => _platformId switch
{
PlatformID.Unix => GetMachineIdLinux(),
PlatformID.MacOSX => ParseMacOsOutput(_getMacOsMachineId()),
PlatformID.Win32NT => _getWindowsMachineId(),
_ => null,
};

private string? GetMachineIdLinux()
{
var paths = _getFilePaths();

foreach (var path in paths)
{
if (File.Exists(path))
{
try
{
return File.ReadAllText(path).Trim();
}
catch (Exception ex)
{
_logger.LogError("Failed to get machine ID on Linux due to {Exception}", ex);
}
}
}

return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,10 @@ internal static class ResourceSemanticConventions
{
public const string AttributeTelemetryDistroName = "telemetry.distro.name";
public const string AttributeTelemetryDistroVersion = "telemetry.distro.version";

public const string AttributeServiceName = "service.name";
public const string AttributeServiceInstanceId = "service.instance.id";

public const string AttributeHostName = "host.name";
public const string AttributeHostId = "host.id";
}
Loading

0 comments on commit 511448e

Please sign in to comment.