diff --git a/sdk/core/Azure.Core/src/Shared/EventSourceEvent.cs b/sdk/core/Azure.Core/src/Shared/EventSourceEvent.cs
new file mode 100644
index 0000000000000..94100e913c897
--- /dev/null
+++ b/sdk/core/Azure.Core/src/Shared/EventSourceEvent.cs
@@ -0,0 +1,75 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.Tracing;
+
+#nullable enable
+
+namespace Azure.Core.Shared
+{
+ ///
+ /// Wraps into simplifying iterating over
+ /// payload properties and providing them to logging libraries in a structured way.
+ ///
+ internal readonly struct EventSourceEvent : IReadOnlyList>
+ {
+ ///
+ /// Gets underlying EventSource event.
+ ///
+ public EventWrittenEventArgs EventData { get; }
+
+ public EventSourceEvent(EventWrittenEventArgs eventData)
+ {
+ EventData = eventData;
+ }
+
+ ///
+ public IEnumerator> GetEnumerator()
+ {
+ if (EventData.PayloadNames == null || EventData.Payload == null)
+ {
+ yield break;
+ }
+
+ for (int i = 0; i < Count; i++)
+ {
+ yield return new KeyValuePair(EventData.PayloadNames[i], EventData.Payload[i]);
+ }
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ ///
+ /// Returns the count of payload properties in the Eventsource event.
+ ///
+ public int Count => EventData.PayloadNames?.Count ?? 0;
+
+ ///
+ /// Formats EventSource event as a string including all payload properties.
+ ///
+ public string Format()
+ {
+ return EventSourceEventFormatting.Format(EventData);
+ }
+
+ ///
+ public KeyValuePair this[int index]
+ {
+ get
+ {
+ if (EventData.PayloadNames == null || EventData.Payload == null || index >= EventData.PayloadNames.Count || index < 0)
+ {
+ throw new IndexOutOfRangeException("Index was out of range.");
+ }
+
+ return new KeyValuePair(EventData.PayloadNames[index], EventData.Payload[index]);
+ }
+ }
+ }
+}
diff --git a/sdk/extensions/Microsoft.Extensions.Azure/src/Internal/AzureEventSourceLogForwarder.cs b/sdk/extensions/Microsoft.Extensions.Azure/src/AzureEventSourceLogForwarder.cs
similarity index 73%
rename from sdk/extensions/Microsoft.Extensions.Azure/src/Internal/AzureEventSourceLogForwarder.cs
rename to sdk/extensions/Microsoft.Extensions.Azure/src/AzureEventSourceLogForwarder.cs
index 1f9cc9a3f8b6c..8aaefe4141a03 100644
--- a/sdk/extensions/Microsoft.Extensions.Azure/src/Internal/AzureEventSourceLogForwarder.cs
+++ b/sdk/extensions/Microsoft.Extensions.Azure/src/AzureEventSourceLogForwarder.cs
@@ -2,9 +2,7 @@
// Licensed under the MIT License.
using System;
-using System.Collections;
using System.Collections.Concurrent;
-using System.Collections.Generic;
using System.Diagnostics.Tracing;
using Azure.Core.Diagnostics;
using Azure.Core.Shared;
@@ -85,36 +83,6 @@ private static LogLevel MapLevel(EventLevel level)
}
}
- private static string FormatMessage(EventSourceEvent eventSourceEvent, Exception exception)
- {
- return EventSourceEventFormatting.Format(eventSourceEvent.EventData);
- }
-
- private readonly struct EventSourceEvent: IReadOnlyList>
- {
- public EventWrittenEventArgs EventData { get; }
-
- public EventSourceEvent(EventWrittenEventArgs eventData)
- {
- EventData = eventData;
- }
-
- public IEnumerator> GetEnumerator()
- {
- for (int i = 0; i < Count; i++)
- {
- yield return new KeyValuePair(EventData.PayloadNames[i], EventData.Payload[i]);
- }
- }
-
- IEnumerator IEnumerable.GetEnumerator()
- {
- return GetEnumerator();
- }
-
- public int Count => EventData.PayloadNames.Count;
-
- public KeyValuePair this[int index] => new KeyValuePair(EventData.PayloadNames[index], EventData.Payload[index]);
- }
+ private static string FormatMessage(EventSourceEvent eventSourceEvent, Exception _) => eventSourceEvent.Format();
}
}
diff --git a/sdk/extensions/Microsoft.Extensions.Azure/src/Microsoft.Extensions.Azure.csproj b/sdk/extensions/Microsoft.Extensions.Azure/src/Microsoft.Extensions.Azure.csproj
index 582b9b87a6106..6b1cac6a63a1b 100644
--- a/sdk/extensions/Microsoft.Extensions.Azure/src/Microsoft.Extensions.Azure.csproj
+++ b/sdk/extensions/Microsoft.Extensions.Azure/src/Microsoft.Extensions.Azure.csproj
@@ -28,6 +28,7 @@
+
diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Azure.Monitor.OpenTelemetry.AspNetCore.csproj b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Azure.Monitor.OpenTelemetry.AspNetCore.csproj
index 362958bcedd19..f23e8e60b3a97 100644
--- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Azure.Monitor.OpenTelemetry.AspNetCore.csproj
+++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Azure.Monitor.OpenTelemetry.AspNetCore.csproj
@@ -1,4 +1,4 @@
-
+
An OpenTelemetry .NET distro that exports to Azure Monitor
AzureMonitor OpenTelemetry ASP.NET Core Distro
@@ -30,10 +30,12 @@
-
-
+
+
+
+
-
+
diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/AzureMonitorAspNetCoreEventSource.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/AzureMonitorAspNetCoreEventSource.cs
index 00e375019bd50..bac1d1f7d0542 100644
--- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/AzureMonitorAspNetCoreEventSource.cs
+++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/AzureMonitorAspNetCoreEventSource.cs
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
-using System;
using System.Diagnostics.Tracing;
using System.Runtime.CompilerServices;
using Azure.Monitor.OpenTelemetry.Exporter.Internals;
@@ -60,6 +59,15 @@ public void GetEnvironmentVariableFailed(string envVarName, Exception ex)
}
}
+ [NonEvent]
+ public void MapLogLevelFailed(EventLevel level)
+ {
+ if (IsEnabled(EventLevel.Warning))
+ {
+ MapLogLevelFailed(level.ToString());
+ }
+ }
+
[Event(1, Message = "Failed to configure AzureMonitorOptions using the connection string from environment variables due to an exception: {0}", Level = EventLevel.Error)]
public void ConfigureFailed(string exceptionMessage) => WriteEvent(1, exceptionMessage);
@@ -74,5 +82,11 @@ public void GetEnvironmentVariableFailed(string envVarName, Exception ex)
[Event(5, Message = "Failed to Read environment variable {0}, exception: {1}", Level = EventLevel.Error)]
public void GetEnvironmentVariableFailed(string envVarName, string exceptionMessage) => WriteEvent(5, envVarName, exceptionMessage);
+
+ [Event(6, Message = "Failed to map unknown EventSource log level in AzureEventSourceLogForwarder {0}", Level = EventLevel.Warning)]
+ public void MapLogLevelFailed(string level) => WriteEvent(6, level);
+
+ [Event(7, Message = "Found existing Microsoft.Extensions.Azure.AzureEventSourceLogForwarder registration.", Level = EventLevel.Informational)]
+ public void LogForwarderIsAlreadyRegistered() => WriteEvent(7);
}
}
diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Internals/AzureSdkCompat/AzureEventSourceLogForwarder.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Internals/AzureSdkCompat/AzureEventSourceLogForwarder.cs
new file mode 100644
index 0000000000000..df510d1a35a12
--- /dev/null
+++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Internals/AzureSdkCompat/AzureEventSourceLogForwarder.cs
@@ -0,0 +1,85 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Collections.Concurrent;
+using System.Diagnostics.Tracing;
+using Azure.Core.Diagnostics;
+using Azure.Core.Shared;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace Azure.Monitor.OpenTelemetry.AspNetCore.Internals.AzureSdkCompat
+{
+ internal sealed class AzureEventSourceLogForwarder : IHostedService, IDisposable
+ {
+ internal static readonly AzureEventSourceLogForwarder Noop = new AzureEventSourceLogForwarder(null);
+ private readonly ILoggerFactory _loggerFactory;
+
+ private readonly ConcurrentDictionary _loggers = new ConcurrentDictionary();
+
+ private readonly Func _formatMessage = FormatMessage;
+
+ private AzureEventSourceListener _listener;
+
+ public AzureEventSourceLogForwarder(ILoggerFactory loggerFactory)
+ {
+ _loggerFactory = loggerFactory;
+ }
+
+ private void LogEvent(EventWrittenEventArgs eventData)
+ {
+ var logger = _loggers.GetOrAdd(eventData.EventSource.Name, name => _loggerFactory!.CreateLogger(ToLoggerName(name)));
+ logger.Log(MapLevel(eventData.Level), new EventId(eventData.EventId, eventData.EventName), new EventSourceEvent(eventData), null, _formatMessage);
+ }
+
+ private static string ToLoggerName(string name)
+ {
+ return name.Replace('-', '.');
+ }
+
+ private static LogLevel MapLevel(EventLevel level)
+ {
+ switch (level)
+ {
+ case EventLevel.Critical:
+ return LogLevel.Critical;
+ case EventLevel.Error:
+ return LogLevel.Error;
+ case EventLevel.Informational:
+ return LogLevel.Information;
+ case EventLevel.Verbose:
+ return LogLevel.Debug;
+ case EventLevel.Warning:
+ return LogLevel.Warning;
+ case EventLevel.LogAlways:
+ return LogLevel.Information;
+ default:
+ AzureMonitorAspNetCoreEventSource.Log.MapLogLevelFailed(level);
+ return LogLevel.None;
+ }
+ }
+
+ private static string FormatMessage(EventSourceEvent eventSourceEvent, Exception _) => eventSourceEvent.Format();
+
+ public Task StartAsync(CancellationToken cancellationToken)
+ {
+ if (_loggerFactory != null)
+ {
+ _listener ??= new AzureEventSourceListener((e, s) => LogEvent(e), EventLevel.Verbose);
+ }
+
+ return Task.CompletedTask;
+ }
+
+ public Task StopAsync(CancellationToken cancellationToken)
+ {
+ _listener?.Dispose();
+ return Task.CompletedTask;
+ }
+
+ public void Dispose()
+ {
+ _listener?.Dispose();
+ }
+ }
+}
diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/OpenTelemetryBuilderExtensions.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/OpenTelemetryBuilderExtensions.cs
index 707847e5d4d1f..689dbfc97b38b 100644
--- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/OpenTelemetryBuilderExtensions.cs
+++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/OpenTelemetryBuilderExtensions.cs
@@ -4,6 +4,7 @@
#nullable enable
using System.Reflection;
+using Azure.Monitor.OpenTelemetry.AspNetCore.Internals.AzureSdkCompat;
using Azure.Monitor.OpenTelemetry.AspNetCore.Internals.Profiling;
using Azure.Monitor.OpenTelemetry.Exporter;
using Azure.Monitor.OpenTelemetry.LiveMetrics;
@@ -184,6 +185,21 @@ public static OpenTelemetryBuilder UseAzureMonitor(this OpenTelemetryBuilder bui
azureMonitorOptions.Get(Options.DefaultName).SetValueToLiveMetricsExporterOptions(exporterOptions);
});
+ // Register Azure SDK log forwarder in the case it was not registered by the user application.
+ builder.Services.AddHostedService(sp =>
+ {
+ var logForwarderType = Type.GetType("Microsoft.Extensions.Azure.AzureEventSourceLogForwarder, Microsoft.Extensions.Azure", false);
+
+ if (logForwarderType != null && sp.GetService(logForwarderType) != null)
+ {
+ AzureMonitorAspNetCoreEventSource.Log.LogForwarderIsAlreadyRegistered();
+ return AzureEventSourceLogForwarder.Noop;
+ }
+
+ var loggerFactory = sp.GetRequiredService();
+ return new AzureEventSourceLogForwarder(loggerFactory);
+ });
+
return builder;
}
diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests.csproj b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests.csproj
index 61e2477d4349f..e5872fb6415a2 100644
--- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests.csproj
+++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests.csproj
@@ -5,6 +5,7 @@
+
@@ -17,6 +18,7 @@
+
diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests/AzureSdkLoggingTests.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests/AzureSdkLoggingTests.cs
new file mode 100644
index 0000000000000..32f5b1bca52d1
--- /dev/null
+++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests/AzureSdkLoggingTests.cs
@@ -0,0 +1,197 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+#if NET6_0_OR_GREATER
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.Tracing;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Azure.Core.Diagnostics;
+using Azure.Core.TestFramework;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Logging;
+using Xunit;
+
+namespace Azure.Monitor.OpenTelemetry.AspNetCore.Tests
+{
+ public class AzureSdkLoggingTests
+ {
+ [Theory]
+ [InlineData(LogLevel.Information, "TestInfoEvent: hello")]
+ [InlineData(LogLevel.Warning, "TestWarningEvent: hello")]
+ [InlineData(LogLevel.Debug, null)]
+ public async Task DistroLogForwarderIsAdded(LogLevel eventLevel, string expectedMessage)
+ {
+ var builder = WebApplication.CreateBuilder();
+ var transport = new MockTransport(new MockResponse(200).SetContent("ok"));
+ SetUpOTelAndLogging(builder, transport, LogLevel.Information);
+
+ using var app = builder.Build();
+ await app.StartAsync();
+
+ using TestEventSource source = new TestEventSource();
+ Assert.True(source.IsEnabled());
+ source.LogMessage("hello", eventLevel);
+ WaitForRequest(transport);
+ if (expectedMessage != null)
+ {
+ Assert.Single(transport.Requests);
+ await AssertContentContains(transport.Requests.Single(), expectedMessage, eventLevel);
+ }
+ else
+ {
+ await AssertContentDoesNotContain(transport.Requests, "hello");
+ }
+ }
+
+ [Theory]
+ [InlineData(LogLevel.Information, "TestInfoEvent: hello")]
+ [InlineData(LogLevel.Warning, "TestWarningEvent: hello")]
+ [InlineData(LogLevel.Debug, null)]
+ public async Task PublicLogForwarderIsAdded(LogLevel eventLevel, string expectedMessage)
+ {
+ var builder = WebApplication.CreateBuilder();
+ var transport = new MockTransport(new MockResponse(200).SetContent("ok"));
+ SetUpOTelAndLogging(builder, transport, LogLevel.Information);
+
+ builder.Services.TryAddSingleton();
+ using var app = builder.Build();
+
+ Microsoft.Extensions.Azure.AzureEventSourceLogForwarder publicLogForwarder =
+ app.Services.GetRequiredService();
+
+ Assert.NotNull(publicLogForwarder);
+ publicLogForwarder.Start();
+
+ await app.StartAsync();
+
+ using TestEventSource source = new TestEventSource();
+ Assert.True(source.IsEnabled());
+ source.LogMessage("hello", eventLevel);
+
+ WaitForRequest(transport);
+ if (expectedMessage != null)
+ {
+ Assert.Single(transport.Requests);
+ await AssertContentContains(transport.Requests.Single(), expectedMessage, eventLevel);
+ }
+ else
+ {
+ await AssertContentDoesNotContain(transport.Requests, "hello");
+ }
+ }
+
+ private void WaitForRequest(MockTransport transport)
+ {
+ SpinWait.SpinUntil(
+ condition: () =>
+ {
+ Thread.Sleep(10);
+ return transport.Requests.Count > 0;
+ },
+ timeout: TimeSpan.FromSeconds(10));
+ }
+
+ private static async Task AssertContentContains(MockRequest request, string expectedMessage, LogLevel expectedLevel)
+ {
+ using var contentStream = new MemoryStream();
+ await request.Content.WriteToAsync(contentStream, default);
+ contentStream.Position = 0;
+ var content = BinaryData.FromStream(contentStream).ToString();
+ var jsonMessage = $"\"message\":\"{expectedMessage}\"";
+ var jsonLevel = $"\"severityLevel\":\"{expectedLevel}\"";
+ Assert.Contains(jsonMessage, content);
+ Assert.Contains(jsonLevel, content);
+
+ // also check that message appears just once
+ Assert.Equal(content.IndexOf(jsonMessage), content.LastIndexOf(jsonMessage));
+ }
+
+ private static async Task AssertContentDoesNotContain(List requests, string expectedMessage)
+ {
+ foreach (var request in requests)
+ {
+ using var contentStream = new MemoryStream();
+ await request.Content.WriteToAsync(contentStream, default);
+ contentStream.Position = 0;
+ var content = BinaryData.FromStream(contentStream).ToString();
+ Console.WriteLine(content);
+
+ Assert.DoesNotContain(expectedMessage, content);
+ }
+ }
+
+ private static void SetUpOTelAndLogging(WebApplicationBuilder builder, MockTransport transport, LogLevel enableLevel)
+ {
+ builder.Logging.ClearProviders();
+ builder.Logging.AddFilter((name, level) =>
+ {
+ if (name != null && name.StartsWith("Azure"))
+ {
+ return level >= enableLevel;
+ }
+ return false;
+ });
+
+ builder.Services.AddOpenTelemetry().UseAzureMonitor(config =>
+ {
+ config.Transport = transport;
+ config.ConnectionString = $"InstrumentationKey={Guid.NewGuid()}";
+ config.EnableLiveMetrics = false;
+ config.Diagnostics.IsDistributedTracingEnabled = false;
+ config.Diagnostics.IsLoggingEnabled = false;
+ });
+ }
+
+ internal class TestEventSource : AzureEventSource
+ {
+ private const string EventSourceName = "Azure-Test";
+ public TestEventSource() : base(EventSourceName)
+ {
+ }
+
+ [Event(1, Level = EventLevel.Informational, Message = "TestInfoEvent: {0}")]
+ public void LogTestInfoEvent(string message)
+ {
+ WriteEvent(1, message);
+ }
+
+ [Event(2, Level = EventLevel.Verbose, Message = "TestVerboseEvent: {0}")]
+ public void LogTestVerboseEvent(string message)
+ {
+ WriteEvent(2, message);
+ }
+
+ [Event(3, Level = EventLevel.Warning, Message = "TestWarningEvent: {0}")]
+ public void LogTestWarningEvent(string message)
+ {
+ WriteEvent(3, message);
+ }
+
+ public void LogMessage(string message, LogLevel level)
+ {
+ switch (level)
+ {
+ case LogLevel.Warning:
+ LogTestWarningEvent(message);
+ break;
+ case LogLevel.Information:
+ LogTestInfoEvent(message);
+ break;
+ case LogLevel.Debug:
+ LogTestVerboseEvent(message);
+ break;
+ default:
+ Assert.Fail("Log level is not supported");
+ break;
+ }
+ }
+ }
+ }
+}
+#endif