Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity, why do we need this change?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we shipped last week, the CI added ApiCompatVersion 1.0.0 to ensure that our Public API doesn't change. (Similar to the PublicApi analyzer we have in other repos).

When I added net6.0, the ApiCompat fails because there was no net6.0 in the 1.0.0 release.
I checked with the Azure Experts and they advised to add this condition; we only need to check the api compat for the framework that we shipped already (netstandard2.0).

When we ship our next version, the CI should regenerate this line without the condition. :)

<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