Skip to content

Commit

Permalink
[AzureMonitorExporter] resolve AOT warnings (#38459)
Browse files Browse the repository at this point in the history
* initial commit

* build fix

* workaround for StackFrame.GetMethod()

* cleanup

* fix test

* remove TrimmingAttribute.cs

* temp disable ApiCompat

* cleanup

* test fix for ApiCompat

* update comment

* add aotcompat test app

* add readme

* readme

* recommended fix for ApiCompat

* comment

* test fix for validation errors re: aotcompat directory

* isolate StackFrame.GetMethod

* cleanup

* isolate StackFrame.GetMethod (2)

* add comment.

* fix

* pr feedback

* update comment

* fix script

* refactor as extension method

* cleanup
  • Loading branch information
TimothyMothra authored Oct 3, 2023
1 parent 9c11169 commit dd0510e
Show file tree
Hide file tree
Showing 14 changed files with 204 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>An OpenTelemetry .NET exporter that exports to Azure Monitor</Description>
<AssemblyTitle>AzureMonitor OpenTelemetry Exporter</AssemblyTitle>
<Version>1.1.0-beta.1</Version>
<!--The ApiCompatVersion is managed automatically and should not generally be modified manually.-->
<ApiCompatVersion>1.0.0</ApiCompatVersion>
<ApiCompatVersion Condition="'$(TargetFramework)' == 'netstandard2.0'">1.0.0</ApiCompatVersion>
<PackageTags>Azure Monitor OpenTelemetry Exporter ApplicationInsights</PackageTags>
<TargetFrameworks>$(RequiredTargetFrameworks)</TargetFrameworks>
<!--NET6 is added here for trimming compatibility. This includes necessary annotations and System.Text.Json's "Source Generation" feature.-->
<TargetFrameworks>net6.0;$(RequiredTargetFrameworks)</TargetFrameworks>
<IncludeOperationsSharedSource>true</IncludeOperationsSharedSource>
</PropertyGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ public StackFrame(System.Diagnostics.StackFrame stackFrame, int frameId)
{
string fullName, assemblyName;

var methodInfo = stackFrame.GetMethod();
var methodInfo = stackFrame.GetMethodWithoutWarning();
if (methodInfo == null)
{
fullName = "unknown";
// In an AOT scenario GetMethod() will return null. Note this can happen even in non AOT scenarios.
// Instead, call ToString() which gives a string like this:
// "MethodName + 0x00 at offset 000 in file:line:column <filename unknown>:0:0"
fullName = stackFrame.ToString();
assemblyName = "unknown";
}
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using System;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Tracing;
using System.Runtime.CompilerServices;
using Azure.Monitor.OpenTelemetry.Exporter.Internals.ConnectionString;
Expand Down Expand Up @@ -87,7 +88,7 @@ public void TransmissionFailed(int statusCode, TelemetryItemOrigin origin, bool
isAadEnabled: isAadEnabled,
instrumentationKey: connectionVars.InstrumentationKey,
configuredEndpoint: connectionVars.IngestionEndpoint,
actualEndpoint: requestEndpoint);
actualEndpoint: requestEndpoint ?? "null");
}
else
{
Expand All @@ -101,7 +102,7 @@ public void TransmissionFailed(int statusCode, TelemetryItemOrigin origin, bool
isAadEnabled: isAadEnabled,
instrumentationKey: connectionVars.InstrumentationKey,
configuredEndpoint: connectionVars.IngestionEndpoint,
actualEndpoint: requestEndpoint);
actualEndpoint: requestEndpoint ?? "null");
}
}
}
Expand All @@ -115,12 +116,13 @@ public void TransmissionFailed(int statusCode, TelemetryItemOrigin origin, bool
isAadEnabled: isAadEnabled,
instrumentationKey: connectionVars.InstrumentationKey,
configuredEndpoint: connectionVars.IngestionEndpoint,
actualEndpoint: requestEndpoint);
actualEndpoint: requestEndpoint ?? "null");
}
}

[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Parameters to this method are primitive and are trimmer safe.")]
[Event(8, Message = "Transmission failed. StatusCode: {0}. Error from Ingestion: {1}. Action: {2}. Origin: {3}. AAD Enabled: {4}. Instrumentation Key: {5}. Configured Endpoint: {6}. Actual Endpoint: {7}", Level = EventLevel.Critical)]
public void TransmissionFailed(int statusCode, string errorMessage, string action, string origin, bool isAadEnabled, string instrumentationKey, string configuredEndpoint, string? actualEndpoint)
public void TransmissionFailed(int statusCode, string errorMessage, string action, string origin, bool isAadEnabled, string instrumentationKey, string configuredEndpoint, string actualEndpoint)
=> WriteEvent(8, statusCode, errorMessage, action, origin, isAadEnabled, instrumentationKey, configuredEndpoint, actualEndpoint);

[Event(9, Message = "{0} has been disposed.", Level = EventLevel.Informational)]
Expand Down Expand Up @@ -213,6 +215,7 @@ public void FailedToExtractActivityEvent(string activitySourceName, string activ
[Event(17, Message = "Failed to extract Activity Event due to an exception. This telemetry item will be lost. ActivitySource: {0}. Activity: {1}. {2}", Level = EventLevel.Error)]
public void FailedToExtractActivityEvent(string activitySourceName, string activityDisplayName, string exceptionMessage) => WriteEvent(17, activitySourceName, activityDisplayName, exceptionMessage);

[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Parameters to this method are primitive and are trimmer safe.")]
[Event(18, Message = "Maximum count of {0} Activity Links reached. Excess Links are dropped. ActivitySource: {1}. Activity: {2}.", Level = EventLevel.Warning)]
public void ActivityLinksIgnored(int maxLinksAllowed, string activitySourceName, string activityDisplayName) => WriteEvent(18, maxLinksAllowed, activitySourceName, activityDisplayName);

Expand Down Expand Up @@ -279,9 +282,11 @@ public void FailedToTransmitFromStorage(bool isAadEnabled, string instrumentatio
}
}

[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Parameters to this method are primitive and are trimmer safe.")]
[Event(25, Message = "Failed to transmit from storage due to an exception. AAD Enabled: {0}. Instrumentation Key: {1}. {2}", Level = EventLevel.Error)]
public void FailedToTransmitFromStorage(bool isAadEnabled, string instrumentationKey, string exceptionMessage) => WriteEvent(25, isAadEnabled, instrumentationKey, exceptionMessage);

[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Parameters to this method are primitive and are trimmer safe.")]
[Event(26, Message = "Successfully transmitted a blob from storage. AAD Enabled: {0}. Instrumentation Key: {1}", Level = EventLevel.Verbose)]
public void TransmitFromStorageSuccess(bool isAadEnabled, string instrumentationKey) => WriteEvent(26, isAadEnabled, instrumentationKey);

Expand Down Expand Up @@ -327,6 +332,7 @@ public void TransmissionSuccess(TelemetryItemOrigin origin, bool isAadEnabled, s
}
}

[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Parameters to this method are primitive and are trimmer safe.")]
[Event(32, Message = "Successfully transmitted a batch of telemetry Items. Origin: {0}. AAD: {1}. Instrumentation Key: {2}", Level = EventLevel.Verbose)]
public void TransmissionSuccess(string origin, bool isAadEnabled, string instrumentationKey) => WriteEvent(32, origin, isAadEnabled, instrumentationKey);

Expand All @@ -339,9 +345,11 @@ public void TransmitterFailed(TelemetryItemOrigin origin, bool isAadEnabled, str
}
}

[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Parameters to this method are primitive and are trimmer safe.")]
[Event(33, Message = "Transmitter failed due to an exception. Origin: {0}. AAD: {1}. Instrumentation Key: {2}. {3}", Level = EventLevel.Error)]
public void TransmitterFailed(string origin, bool isAadEnabled, string instrumentationKey, string exceptionMessage) => WriteEvent(33, origin, isAadEnabled, instrumentationKey, exceptionMessage);

[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Parameters to this method are primitive and are trimmer safe.")]
[Event(34, Message = "Exporter encountered a transmission failure and will wait {0} milliseconds before transmitting again.", Level = EventLevel.Warning)]
public void BackoffEnabled(double milliseconds) => WriteEvent(34, milliseconds);

Expand Down Expand Up @@ -372,6 +380,7 @@ public void PartialContentResponseInvalid(int totalTelemetryItems, TelemetryErro
}
}

[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Parameters to this method are primitive and are trimmer safe.")]
[Event(38, Message = "Received a partial success from ingestion that does not match telemetry that was sent. Total telemetry items sent: {0}. Error Index: {1}. Error StatusCode: {2}. Error Message: {3}", Level = EventLevel.Warning)]
public void PartialContentResponseInvalid(int totalTelemetryItems, string errorIndex, string errorStatusCode, string errorMessage) => WriteEvent(38, totalTelemetryItems, errorIndex, errorStatusCode, errorMessage);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@ internal static class IngestionResponseHelper

var responseContent = response.Content.ToString();

var responseObj = JsonSerializer.Deserialize<ResponseObject>(responseContent);
ResponseObject? responseObj;

#if NET6_0_OR_GREATER
responseObj = JsonSerializer.Deserialize<ResponseObject>(responseContent, SourceGenerationContext.Default.ResponseObject);
#else
responseObj = JsonSerializer.Deserialize<ResponseObject>(responseContent);
#endif

if (responseObj == null || responseObj.Errors == null)
{
Expand All @@ -39,7 +45,8 @@ internal static class IngestionResponseHelper
}
}

private class ResponseObject
// This class needs to be internal rather than private so that it can be used by the System.Text.Json source generator
internal class ResponseObject
{
[JsonPropertyName("itemsReceived")]
public int ItemsReceived { get; set; }
Expand All @@ -51,7 +58,8 @@ private class ResponseObject
public List<ErrorObject>? Errors { get; set; }
}

private class ErrorObject
// This class needs to be internal rather than private so that it can be used by the System.Text.Json source generator
internal class ErrorObject
{
[JsonPropertyName("index")]
public int Index { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,17 @@ internal static string GetProblemId(Exception exception)

if (exceptionStackFrame != null)
{
MethodBase? methodBase = exceptionStackFrame.GetMethod();
MethodBase? methodBase = exceptionStackFrame.GetMethodWithoutWarning();

if (methodBase != null)
if (methodBase == null)
{
// In an AOT scenario GetMethod() will return null.
// Instead, call ToString() which gives a string like this:
// "MethodName + 0x00 at offset 000 in file:line:column <filename unknown>:0:0"
methodName = exceptionStackFrame.ToString();
methodOffset = System.Diagnostics.StackFrame.OFFSET_UNKNOWN;
}
else
{
methodName = (methodBase.DeclaringType?.FullName ?? "Global") + "." + methodBase.Name;
methodOffset = exceptionStackFrame.GetILOffset();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Azure.Monitor.OpenTelemetry.Exporter.Internals.Statsbeat;
using System.Text.Json.Serialization;

namespace Azure.Monitor.OpenTelemetry.Exporter.Internals;

#if NET6_0_OR_GREATER
// "Source Generation" is feature added to System.Text.Json in .NET 6.0.
// This is a performance optimization that avoids runtime reflection when performing serialization.
// Serialization metadata will be computed at compile-time and included in the assembly.
// https://learn.microsoft.com/dotnet/standard/serialization/system-text-json/source-generation-modes
// https://learn.microsoft.com/dotnet/standard/serialization/system-text-json/source-generation
// https://devblogs.microsoft.com/dotnet/try-the-new-system-text-json-source-generator/
[JsonSerializable(typeof(VmMetadataResponse))]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(IngestionResponseHelper.ResponseObject))]
[JsonSerializable(typeof(IngestionResponseHelper.ErrorObject))]
[JsonSerializable(typeof(int))]
internal partial class SourceGenerationContext : JsonSerializerContext
{
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.CompilerServices;

namespace Azure.Monitor.OpenTelemetry.Exporter.Internals
{
internal static class StackFrameExtensions
{
/// <summary>
/// Wrapper for <see cref="System.Diagnostics.StackFrame.GetMethod"/>.
/// This method disables the Trimmer warning "IL2026:RequiresUnreferencedCode".
/// Callers MUST handle the null condition.
/// </summary>
/// <remarks>
/// In an AOT scenario GetMethod() will return null. Note this can happen even in non AOT scenarios.
/// Instead, call ToString() which gives a string like this:
/// "MethodName + 0x00 at offset 000 in file:line:column filename unknown:0:0".
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "We will handle the null condition and call ToString() instead.")]
public static MethodBase? GetMethodWithoutWarning(this System.Diagnostics.StackFrame stackFrame) => stackFrame.GetMethod();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,12 @@ private Measurement<int> GetAttachStatsbeat()
{
httpClient.DefaultRequestHeaders.Add("Metadata", "True");
var responseString = httpClient.GetStringAsync(StatsbeatConstants.AMS_Url);
var vmMetadata = JsonSerializer.Deserialize<VmMetadataResponse>(responseString.Result);
VmMetadataResponse? vmMetadata;
#if NET6_0_OR_GREATER
vmMetadata = JsonSerializer.Deserialize<VmMetadataResponse>(responseString.Result, SourceGenerationContext.Default.VmMetadataResponse);
#else
vmMetadata = JsonSerializer.Deserialize<VmMetadataResponse>(responseString.Result);
#endif

return vmMetadata;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

namespace Azure.Monitor.OpenTelemetry.Exporter.Internals.Statsbeat
{
// This class needs to be internal rather than private so that it can be used by the System.Text.Json source generator
internal class VmMetadataResponse
{
public string? osType { get; set; }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net7.0;net6.0;net462</TargetFrameworks>
<IsTestSupportProject>true</IsTestSupportProject>
</PropertyGroup>

<PropertyGroup Condition="'$(TargetFramework)' == 'net7.0'">
<PublishAot>true</PublishAot>
<TrimmerSingleWarn>false</TrimmerSingleWarn>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\..\core\Azure.Core\src\Azure.Core.csproj" />
<ProjectReference Include="..\..\src\Azure.Monitor.OpenTelemetry.Exporter.csproj" />

<!-- Update this dependency to its latest, which has all the annotations -->
<PackageReference Include="Microsoft.Extensions.Logging.Configuration" VersionOverride="8.0.0-rc.1.23419.4" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
<TrimmerRootAssembly Include="Azure.Monitor.OpenTelemetry.Exporter" />
</ItemGroup>

<!--Temp fix for "error : No files matching ;NU5105;CA1812;"-->
<!--Real fix coming in .NET 8 RC2: https://github.com/dotnet/runtime/issues/91965-->
<ItemGroup>
<_NoWarn Include="$(NoWarn)" />
</ItemGroup>
<PropertyGroup>
<NoWarn>@(_NoWarn)</NoWarn>
</PropertyGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

namespace Azure.Monitor.OpenTelemetry.Exporter.AotCompatibilityTestApp;

internal class Program
{
public static void Main(string[] args)
{
System.Console.WriteLine("Hello, World!");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
param([string]$targetNetFramework)

dotnet restore
$publishOutput = dotnet publish --framework net7.0 -nodeReuse:false /p:UseSharedCompilation=false /p:ExposeExperimentalFeatures=true

if ($LASTEXITCODE -ne 0)
{
Write-Host "Publish failed."
Write-Host $publishOutput
Exit 2
}

$actualWarningCount = 0

foreach ($line in $($publishOutput -split "`r`n"))
{
if ($line -like "*analysis warning IL*")
{
Write-Host $line

$actualWarningCount += 1
}
}

Write-Host "Actual warning count is:", $actualWarningCount
$expectedWarningCount = 7
# Known warnings:
# - Azure.Core.Serialization.DynamicData: Using member 'Azure.Core.Serialization.DynamicData.DynamicDataJsonConverter.DynamicDataJsonConverter()'
# - 4x Azure.RequestFailedException.TryExtractErrorContent(): Using member 'System.Text.Json.JsonSerializer.Deserialize<>()' (https://github.com/Azure/azure-sdk-for-net/pull/38996)
# - Azure.Core.Json.MutableJsonDocument: Using member 'Azure.Core.Json.MutableJsonDocument.MutableJsonDocumentConverter.MutableJsonDocumentConverter()'
# - Microsoft.Extensions.DependencyInjection.ServiceLookup.IEnumerableCallSite.ServiceType.get: Using member 'System.Type.MakeGenericType(Type[])

$testPassed = 0
if ($actualWarningCount -ne $expectedWarningCount)
{
$testPassed = 1
Write-Host "Actual warning count:", $actualWarningCount, "is not as expected. Expected warning count is:", $expectedWarningCount
}

Exit $testPassed
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,15 @@ public void TestNullMethodInfoInStack()
#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type.
#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.

// In AOT, StackFrame.GetMethod() can return null.
// In this instance, we fall back to StackFrame.ToString()
frameMock.Setup(x => x.ToString()).Returns("MethodName + 0x00 at offset 000 in file:line:column <filename unknown>:0:0");

Models.StackFrame stackFrame = new Models.StackFrame(frameMock.Object, 0);

Assert.Equal("unknown", stackFrame.Assembly);
Assert.Null(stackFrame.FileName);
Assert.Equal("unknown", stackFrame.Method);
Assert.Equal("MethodName + 0x00 at offset 000 in file:line:column <filename unknown>:0:0", stackFrame.Method);
Assert.Null(stackFrame.Line);
}

Expand Down
Loading

0 comments on commit dd0510e

Please sign in to comment.