diff --git a/Logging.sln b/Logging.sln index 8abc07d7..a896def2 100644 --- a/Logging.sln +++ b/Logging.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27130.2036 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.28315.86 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{EE933574-C82B-4E59-A0D1-05328197B937}" EndProject @@ -63,6 +63,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NLogTarget.NetCoreApp10.Tes EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Log4NetAppender.NetCoreApp10.Tests", "test\Log4NetAppender.NetCoreApp10.Tests\Log4NetAppender.NetCoreApp10.Tests.csproj", "{F74826DB-53B1-4588-BD02-4DD25DCBF292}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ILogger", "src\ILogger\ILogger.csproj", "{7D942802-E85E-4C3C-B97E-67A3867B7CBA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ILogger.NetStandard.Tests", "test\ILogger.NetStandard.Tests\ILogger.NetStandard.Tests.csproj", "{4C1B862F-8F19-4D0F-834D-694DDE0438AE}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution test\CommonTestShared\CommonTestShared.projitems*{3b9ab7fa-562d-4e4e-86e3-3348426bc0d9}*SharedItemsImports = 13 @@ -225,6 +229,22 @@ Global {F74826DB-53B1-4588-BD02-4DD25DCBF292}.Release|Any CPU.Build.0 = Release|Any CPU {F74826DB-53B1-4588-BD02-4DD25DCBF292}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {F74826DB-53B1-4588-BD02-4DD25DCBF292}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {7D942802-E85E-4C3C-B97E-67A3867B7CBA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7D942802-E85E-4C3C-B97E-67A3867B7CBA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7D942802-E85E-4C3C-B97E-67A3867B7CBA}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {7D942802-E85E-4C3C-B97E-67A3867B7CBA}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {7D942802-E85E-4C3C-B97E-67A3867B7CBA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7D942802-E85E-4C3C-B97E-67A3867B7CBA}.Release|Any CPU.Build.0 = Release|Any CPU + {7D942802-E85E-4C3C-B97E-67A3867B7CBA}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {7D942802-E85E-4C3C-B97E-67A3867B7CBA}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {4C1B862F-8F19-4D0F-834D-694DDE0438AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4C1B862F-8F19-4D0F-834D-694DDE0438AE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4C1B862F-8F19-4D0F-834D-694DDE0438AE}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {4C1B862F-8F19-4D0F-834D-694DDE0438AE}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {4C1B862F-8F19-4D0F-834D-694DDE0438AE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4C1B862F-8F19-4D0F-834D-694DDE0438AE}.Release|Any CPU.Build.0 = Release|Any CPU + {4C1B862F-8F19-4D0F-834D-694DDE0438AE}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {4C1B862F-8F19-4D0F-834D-694DDE0438AE}.Release|Mixed Platforms.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -251,6 +271,8 @@ Global {11E4A92F-154E-450B-8508-437C28744BC2} = {2FCC45B3-D820-405D-87FA-467C96465BB1} {B24CB3C6-A3A0-4190-97E4-E847C7B1A691} = {2FCC45B3-D820-405D-87FA-467C96465BB1} {F74826DB-53B1-4588-BD02-4DD25DCBF292} = {2FCC45B3-D820-405D-87FA-467C96465BB1} + {7D942802-E85E-4C3C-B97E-67A3867B7CBA} = {EE933574-C82B-4E59-A0D1-05328197B937} + {4C1B862F-8F19-4D0F-834D-694DDE0438AE} = {2FCC45B3-D820-405D-87FA-467C96465BB1} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {AC8888B1-0E98-49D7-BE15-EB97A6261C0D} diff --git a/src/ILogger/ApplicationInsightsLogger.cs b/src/ILogger/ApplicationInsightsLogger.cs new file mode 100644 index 00000000..0ab4f0c3 --- /dev/null +++ b/src/ILogger/ApplicationInsightsLogger.cs @@ -0,0 +1,193 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. 2013 +// +// ----------------------------------------------------------------------- + +namespace Microsoft.Extensions.Logging.ApplicationInsights +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Text; + using Microsoft.ApplicationInsights; + using Microsoft.ApplicationInsights.DataContracts; + + /// + /// Application insights logger implementation for . + /// + /// + public class ApplicationInsightsLogger : ILogger + { + private readonly string categoryName; + private readonly TelemetryClient telemetryClient; + private readonly ApplicationInsightsLoggerOptions applicationInsightsLoggerOptions; + + /// + /// Creates a new instance of . + /// + public ApplicationInsightsLogger( + string categoryName, + TelemetryClient telemetryClient, + ApplicationInsightsLoggerOptions applicationInsightsLoggerOptions) + { + this.categoryName = categoryName; + this.telemetryClient = telemetryClient; + this.applicationInsightsLoggerOptions = applicationInsightsLoggerOptions ?? throw new ArgumentNullException(nameof(applicationInsightsLoggerOptions)); + } + + /// + /// Gets or sets the external scope provider. + /// + internal IExternalScopeProvider ExternalScopeProvider { get; set; } + + /// + /// Begins a logical operation scope. + /// + /// Current state. + /// The identifier for the scope. + /// + /// An IDisposable that ends the logical operation scope on dispose. + /// + public IDisposable BeginScope(TState state) + { + return this.ExternalScopeProvider != null ? this.ExternalScopeProvider.Push(state) : NullScope.Instance; + } + + /// + /// Checks if the given is enabled. + /// + /// level to be checked. + /// + /// true if enabled. + /// + public bool IsEnabled(LogLevel logLevel) + { + return this.telemetryClient.IsEnabled(); + } + + /// + /// Writes a log entry. + /// + /// State being passed along. + /// Entry will be written on this level. + /// Id of the event. + /// The entry to be written. Can be also an object. + /// The exception related to this entry. + /// Function to create a string message of the and . + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + if (this.IsEnabled(logLevel)) + { + if (exception == null || !this.applicationInsightsLoggerOptions.TrackExceptionsAsExceptionTelemetry) + { + TraceTelemetry traceTelemetry = new TraceTelemetry( + formatter(state, exception), + ApplicationInsightsLogger.GetSeverityLevel(logLevel)); + this.PopulateTelemetry(traceTelemetry, state, eventId); + this.telemetryClient.TrackTrace(traceTelemetry); + } + else + { + ExceptionTelemetry exceptionTelemetry = new ExceptionTelemetry(exception) + { + Message = formatter(state, exception), + SeverityLevel = ApplicationInsightsLogger.GetSeverityLevel(logLevel), + }; + + this.PopulateTelemetry(exceptionTelemetry, state, eventId); + this.telemetryClient.TrackException(exceptionTelemetry); + } + } + } + + /// + /// Converts the into corresponding Application insights . + /// + /// Logging log level. + /// Application insights corresponding SeverityLevel for the LogLevel. + private static SeverityLevel GetSeverityLevel(LogLevel logLevel) + { + switch (logLevel) + { + case LogLevel.Critical: + return SeverityLevel.Critical; + case LogLevel.Error: + return SeverityLevel.Error; + case LogLevel.Warning: + return SeverityLevel.Warning; + case LogLevel.Information: + return SeverityLevel.Information; + case LogLevel.Debug: + case LogLevel.Trace: + default: + return SeverityLevel.Verbose; + } + } + + /// + /// Populates the state, scope and event information for the logging event. + /// + /// State information for the current event. + /// Telemetry item. + /// Event state information. + /// Event Id information. + private void PopulateTelemetry(ISupportProperties telemetryItem, TState state, EventId eventId) + { + IDictionary dict = telemetryItem.Properties; + dict["CategoryName"] = this.categoryName; + + if (eventId.Id != 0) + { + dict["EventId"] = eventId.Id.ToString(CultureInfo.InvariantCulture); + } + + if (!string.IsNullOrEmpty(eventId.Name)) + { + dict["EventName"] = eventId.Name; + } + + if (this.applicationInsightsLoggerOptions.IncludeScopes) + { + if (state is IReadOnlyCollection> stateDictionary) + { + foreach (KeyValuePair item in stateDictionary) + { + dict[item.Key] = Convert.ToString(item.Value, CultureInfo.InvariantCulture); + } + } + + if (this.ExternalScopeProvider != null) + { + StringBuilder stringBuilder = new StringBuilder(); + this.ExternalScopeProvider.ForEachScope( + (activeScope, builder) => + { + // Ideally we expect that the scope to implement IReadOnlyList>. + // But this is not guaranteed as user can call BeginScope and pass anything. Hence + // we try to resolve the scope as Dictionary and if we fail, we just serialize the object and add it. + + if (activeScope is IReadOnlyCollection> activeScopeDictionary) + { + foreach (KeyValuePair item in activeScopeDictionary) + { + dict[item.Key] = Convert.ToString(item.Value, CultureInfo.InvariantCulture); + } + } + else + { + builder.Append(" => ").Append(activeScope); + } + }, + stringBuilder); + + if (stringBuilder.Length > 0) + { + dict["Scope"] = stringBuilder.ToString(); + } + } + } + } + } +} diff --git a/src/ILogger/ApplicationInsightsLoggerOptions.cs b/src/ILogger/ApplicationInsightsLoggerOptions.cs new file mode 100644 index 00000000..838d9b3c --- /dev/null +++ b/src/ILogger/ApplicationInsightsLoggerOptions.cs @@ -0,0 +1,30 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. 2013 +// +// ----------------------------------------------------------------------- + +namespace Microsoft.Extensions.Logging.ApplicationInsights +{ + using Microsoft.ApplicationInsights.DataContracts; + using Microsoft.Extensions.Logging; + + /// + /// defines the custom behavior of the tracing information sent to Application Insights. + /// + public class ApplicationInsightsLoggerOptions + { + /// + /// Gets or sets a value indicating whether to track exceptions as . + /// Defaults to true. + /// + public bool TrackExceptionsAsExceptionTelemetry { get; set; } = true; + + /// + /// Gets or sets a value indicating whether the Scope information is included from telemetry or not. + /// Defaults to true. + /// + public bool IncludeScopes { get; set; } = true; + } +} \ No newline at end of file diff --git a/src/ILogger/ApplicationInsightsLoggerProvider.cs b/src/ILogger/ApplicationInsightsLoggerProvider.cs new file mode 100644 index 00000000..05f2a024 --- /dev/null +++ b/src/ILogger/ApplicationInsightsLoggerProvider.cs @@ -0,0 +1,111 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. 2013 +// +// ----------------------------------------------------------------------- + +namespace Microsoft.Extensions.Logging.ApplicationInsights +{ + using System; + using System.Collections.Concurrent; + using Microsoft.ApplicationInsights; + using Microsoft.ApplicationInsights.Extensibility; + using Microsoft.ApplicationInsights.Extensibility.Implementation; + using Microsoft.ApplicationInsights.Implementation; + using Microsoft.Extensions.Options; + + /// + /// Application insights logger provider. + /// + /// + /// + [ProviderAlias("ApplicationInsights")] + public class ApplicationInsightsLoggerProvider : ILoggerProvider, ISupportExternalScope + { + /// + /// The application insights logger options. + /// + private readonly ApplicationInsightsLoggerOptions applicationInsightsLoggerOptions; + + /// + /// The telemetry client to be used to log messages to Application Insights. + /// + private readonly TelemetryClient telemetryClient; + + /// + /// The external scope provider to allow setting scope data in messages. + /// + private IExternalScopeProvider externalScopeProvider; + + /// + /// Initializes a new instance of the class. + /// + /// The telemetry configuration options.. + /// The application insights logger options. + /// + /// telemetryConfiguration + /// or + /// loggingFilter + /// or + /// applicationInsightsLoggerOptions. + /// + public ApplicationInsightsLoggerProvider( + IOptions telemetryConfigurationOptions, + IOptions applicationInsightsLoggerOptions) + { + if (telemetryConfigurationOptions?.Value == null) + { + throw new ArgumentNullException(nameof(telemetryConfigurationOptions)); + } + + this.applicationInsightsLoggerOptions = applicationInsightsLoggerOptions?.Value ?? throw new ArgumentNullException(nameof(applicationInsightsLoggerOptions)); + + this.telemetryClient = new TelemetryClient(telemetryConfigurationOptions.Value); + this.telemetryClient.Context.GetInternalContext().SdkVersion = SdkVersionUtils.GetSdkVersion("il:"); + } + + /// + /// Creates a new instance. + /// + /// The category name for messages produced by the logger. + /// An instance to be used for logging. + public ILogger CreateLogger(string categoryName) + { + return new ApplicationInsightsLogger( + categoryName, + this.telemetryClient, + this.applicationInsightsLoggerOptions) + { + ExternalScopeProvider = this.externalScopeProvider, + }; + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Sets the scope provider. This method also updates all the existing logger to also use the new ScopeProvider. + /// + /// The external scope provider. + public void SetScopeProvider(IExternalScopeProvider externalScopeProvider) + { + this.externalScopeProvider = externalScopeProvider; + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// Release managed resources. + protected virtual void Dispose(bool releasedManagedResources) + { + // Nothing to dispose right now. + } + } +} diff --git a/src/ILogger/ApplicationInsightsLoggingBuilderExtensions.cs b/src/ILogger/ApplicationInsightsLoggingBuilderExtensions.cs new file mode 100644 index 00000000..80e65e2f --- /dev/null +++ b/src/ILogger/ApplicationInsightsLoggingBuilderExtensions.cs @@ -0,0 +1,102 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. 2013 +// +// ----------------------------------------------------------------------- + +namespace Microsoft.Extensions.Logging +{ + using System; + using Microsoft.ApplicationInsights.Extensibility; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.DependencyInjection.Extensions; + using Microsoft.Extensions.Logging.ApplicationInsights; + + /// + /// Extensions methods to add and configure application insights logger. + /// + public static class ApplicationInsightsLoggingBuilderExtensions + { + /// + /// Adds an ApplicationInsights logger named 'ApplicationInsights' to the factory. + /// + /// The to use. + /// Logging builder with Application Insights added to it. + public static ILoggingBuilder AddApplicationInsights(this ILoggingBuilder builder) + { + return builder.AddApplicationInsights((applicationInsightsOptions) => { }); + } + + /// + /// Adds an ApplicationInsights logger named 'ApplicationInsights' to the factory. + /// + /// The to use. + /// Application insights instrumentation key. + /// Logging builder with Application Insights added to it. + public static ILoggingBuilder AddApplicationInsights( + this ILoggingBuilder builder, + string instrumentationKey) + { + return builder.AddApplicationInsights( + (telemetryConfiguration) => telemetryConfiguration.InstrumentationKey = instrumentationKey, + (applicationInsightsOptions) => { }); + } + + /// + /// Adds an ApplicationInsights logger named 'ApplicationInsights' to the factory. + /// + /// The to use. + /// Application insights instrumentation key. + /// Action to configure ApplicationInsights logger. + /// Logging builder with Application Insights added to it. + public static ILoggingBuilder AddApplicationInsights( + this ILoggingBuilder builder, + string instrumentationKey, + Action configureApplicationInsightsLoggerOptions) + { + return builder.AddApplicationInsights( + (telemetryConfiguration) => telemetryConfiguration.InstrumentationKey = instrumentationKey, + configureApplicationInsightsLoggerOptions); + } + + /// + /// Adds an ApplicationInsights logger named 'ApplicationInsights' to the factory. + /// + /// The to use. + /// Action to configure ApplicationInsights logger. + public static ILoggingBuilder AddApplicationInsights( + this ILoggingBuilder builder, + Action configureApplicationInsightsLoggerOptions) + { + return builder.AddApplicationInsights( + (telemetryConfiguration) => { }, + configureApplicationInsightsLoggerOptions); + } + + /// + /// Adds an ApplicationInsights logger named 'ApplicationInsights' to the factory. + /// + /// The to use. + /// Action to configure telemetry configuration. + /// Action to configure ApplicationInsights logger. + private static ILoggingBuilder AddApplicationInsights( + this ILoggingBuilder builder, + Action configureTelemetryConfiguration, + Action configureApplicationInsightsLoggerOptions) + { + if (configureApplicationInsightsLoggerOptions == null) + { + throw new ArgumentNullException(nameof(configureApplicationInsightsLoggerOptions)); + } + + // Initialize IOptions user can keep on configuring it furthur if they want to. + builder.Services.Configure(configureTelemetryConfiguration); + + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.Configure(configureApplicationInsightsLoggerOptions); + + return builder; + } + } +} diff --git a/src/ILogger/ILogger.csproj b/src/ILogger/ILogger.csproj new file mode 100644 index 00000000..12157ee8 --- /dev/null +++ b/src/ILogger/ILogger.csproj @@ -0,0 +1,67 @@ + + + netstandard2.0 + Microsoft.Extensions.Logging.ApplicationInsights + Microsoft.Extensions.Logging.ApplicationInsights + false + false + false + false + false + + true + + + true + + + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + + + + + + + + True + True + Microsoft.Extensions.Logging.ApplicationInsights + Microsoft Logging Extensions for ApplicationInsights + Application Insights ILogger allows forwarding events from ILogger to Application Insights. Application Insights will collect your logs from multiple sources and provide rich powerful search capabilities. Privacy statement: https://go.microsoft.com/fwlink/?LinkId=512156 + $(PackageTags) ILogger ILoggerBuilder ILoggerProvider + + + + full + true + + + + false + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + All + + + + + + + + diff --git a/src/ILogger/NullScope.cs b/src/ILogger/NullScope.cs new file mode 100644 index 00000000..72edec41 --- /dev/null +++ b/src/ILogger/NullScope.cs @@ -0,0 +1,32 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. 2013 +// +// ----------------------------------------------------------------------- + +namespace Microsoft.Extensions.Logging.ApplicationInsights +{ + using System; + + /// + /// An empty scope without any logic. + /// + internal class NullScope : IDisposable + { + private NullScope() + { + } + + public static NullScope Instance { get; } = new NullScope(); + +#pragma warning disable CA1063 // Implement IDisposable Correctly - Nothing at all to dispose. + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() +#pragma warning restore CA1063 // Implement IDisposable Correctly - Nothing at all to dispose. + { + } + } +} diff --git a/test/ILogger.NetStandard.Tests/ILogger.NetStandard.Tests.csproj b/test/ILogger.NetStandard.Tests/ILogger.NetStandard.Tests.csproj new file mode 100644 index 00000000..d26bb7fc --- /dev/null +++ b/test/ILogger.NetStandard.Tests/ILogger.NetStandard.Tests.csproj @@ -0,0 +1,25 @@ + + + + netcoreapp2.1 + false + false + + + + + + + + + + + + + + + + + + + diff --git a/test/ILogger.NetStandard.Tests/ILoggerIntegrationTests.cs b/test/ILogger.NetStandard.Tests/ILoggerIntegrationTests.cs new file mode 100644 index 00000000..4c057c04 --- /dev/null +++ b/test/ILogger.NetStandard.Tests/ILoggerIntegrationTests.cs @@ -0,0 +1,302 @@ +// +// Copyright © Microsoft. All Rights Reserved. +// + +namespace Microsoft.ApplicationInsights +{ + using System; + using System.Collections.Generic; + using System.Text; + using Microsoft.ApplicationInsights.Channel; + using Microsoft.ApplicationInsights.DataContracts; + using Microsoft.ApplicationInsights.Extensibility; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Logging.ApplicationInsights; + using Microsoft.Extensions.Options; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + /// + /// Tests the integration for Application Insights. + /// + [TestClass] + public class ILoggerIntegrationTests + { + /// + /// Ensures that is invoked when user logs using . + /// + [TestMethod] + [TestCategory("ILogger")] + public void ApplicationInsightsLoggerIsInvokedWhenUsingILogger() + { + List itemsReceived = new List(); + + IServiceProvider serviceProvider = ILoggerIntegrationTests.SetupApplicationInsightsLoggerIntegration((telemetryItem, telemetryProcessor) => + { + itemsReceived.Add(telemetryItem); + }); + + ILogger testLogger = serviceProvider.GetRequiredService>(); + + testLogger.LogInformation("Testing"); + testLogger.LogError(new Exception("TestException"), "Exception"); + testLogger.LogInformation(new EventId(100, "TestEvent"), "TestingEvent"); + testLogger.LogCritical("Critical"); + testLogger.LogTrace("Trace"); + testLogger.LogWarning("Warning"); + testLogger.LogDebug("Debug"); + + Assert.AreEqual(7, itemsReceived.Count); + + Assert.IsTrue((itemsReceived[2] as ISupportProperties).Properties.ContainsKey("EventId")); + Assert.IsTrue((itemsReceived[2] as ISupportProperties).Properties.ContainsKey("EventName")); + Assert.AreEqual("100", (itemsReceived[2] as ISupportProperties).Properties["EventId"]); + Assert.AreEqual("TestEvent", (itemsReceived[2] as ISupportProperties).Properties["EventName"]); + + Assert.AreEqual("Microsoft.ApplicationInsights.ILoggerIntegrationTests", (itemsReceived[2] as ISupportProperties).Properties["CategoryName"]); + Assert.AreEqual("Microsoft.ApplicationInsights.ILoggerIntegrationTests", (itemsReceived[0] as ISupportProperties).Properties["CategoryName"]); + + Assert.AreEqual(SeverityLevel.Information, (itemsReceived[0] as TraceTelemetry).SeverityLevel); + Assert.AreEqual(SeverityLevel.Error, (itemsReceived[1] as ExceptionTelemetry).SeverityLevel); + Assert.AreEqual(SeverityLevel.Information, (itemsReceived[2] as TraceTelemetry).SeverityLevel); + Assert.AreEqual(SeverityLevel.Critical, (itemsReceived[3] as TraceTelemetry).SeverityLevel); + Assert.AreEqual(SeverityLevel.Verbose, (itemsReceived[4] as TraceTelemetry).SeverityLevel); + Assert.AreEqual(SeverityLevel.Warning, (itemsReceived[5] as TraceTelemetry).SeverityLevel); + Assert.AreEqual(SeverityLevel.Verbose, (itemsReceived[6] as TraceTelemetry).SeverityLevel); + + Assert.AreEqual("Testing", (itemsReceived[0] as TraceTelemetry).Message); + Assert.AreEqual("Exception", (itemsReceived[1] as ExceptionTelemetry).Message); + Assert.AreEqual("TestingEvent", (itemsReceived[2] as TraceTelemetry).Message); + Assert.AreEqual("Critical", (itemsReceived[3] as TraceTelemetry).Message); + Assert.AreEqual("Trace", (itemsReceived[4] as TraceTelemetry).Message); + Assert.AreEqual("Warning", (itemsReceived[5] as TraceTelemetry).Message); + Assert.AreEqual("Debug", (itemsReceived[6] as TraceTelemetry).Message); + } + + /// + /// Ensures that the switch is honored + /// and exceptions are logged as trace messages when value is true. + /// + [TestMethod] + [TestCategory("ILogger")] + public void ApplicationInsightsLoggerLogsExceptionAsExceptionWhenSwitchIsTrue() + { + List itemsReceived = new List(); + + // Case where Exceptions are logged as Exception Telemetry. + IServiceProvider serviceProvider = ILoggerIntegrationTests.SetupApplicationInsightsLoggerIntegration( + (telemetryItem, telemetryProcessor) => itemsReceived.Add(telemetryItem), + configureTelemetryConfiguration: null, + configureApplicationInsightsOptions: (appInsightsLoggerOptions) => appInsightsLoggerOptions.TrackExceptionsAsExceptionTelemetry = true); + + ILogger testLogger = serviceProvider.GetRequiredService>(); + + testLogger.LogInformation("Testing"); + testLogger.LogError(new Exception("TestException"), "Exception"); + + Assert.IsInstanceOfType(itemsReceived[0], typeof(TraceTelemetry)); + Assert.IsInstanceOfType(itemsReceived[1], typeof(ExceptionTelemetry)); + + Assert.AreEqual("Testing", (itemsReceived[0] as TraceTelemetry).Message); + Assert.AreEqual("Exception", (itemsReceived[1] as ExceptionTelemetry).Message); + } + + /// + /// Ensures that the switch is honored + /// and exceptions are logged as trace messages when value is false. + /// + [TestMethod] + [TestCategory("ILogger")] + public void ApplicationInsightsLoggerLogsExceptionAsTraceWhenSwitchIsFalse() + { + List itemsReceived = new List(); + + IServiceProvider serviceProvider = ILoggerIntegrationTests.SetupApplicationInsightsLoggerIntegration( + (telemetryItem, telemetryProcessor) => itemsReceived.Add(telemetryItem), + configureTelemetryConfiguration: null, + configureApplicationInsightsOptions: (appInsightsLoggerOptions) => appInsightsLoggerOptions.TrackExceptionsAsExceptionTelemetry = false); + + ILogger testLogger = serviceProvider.GetRequiredService>(); + + testLogger.LogInformation("Testing"); + testLogger.LogError(new Exception("TestException"), "Exception"); + + Assert.IsInstanceOfType(itemsReceived[0], typeof(TraceTelemetry)); + Assert.IsInstanceOfType(itemsReceived[1], typeof(TraceTelemetry)); + + Assert.AreEqual(SeverityLevel.Information, (itemsReceived[0] as TraceTelemetry).SeverityLevel); + Assert.AreEqual("Testing", (itemsReceived[0] as TraceTelemetry).Message); + + Assert.AreEqual(SeverityLevel.Error, (itemsReceived[1] as TraceTelemetry).SeverityLevel); + Assert.AreEqual("Exception", (itemsReceived[1] as TraceTelemetry).Message); + } + + /// + /// Ensures that the switch is honored and scopes are added + /// when switch is true. + /// + [TestMethod] + [TestCategory("ILogger")] + public void ApplicationInsightsLoggerAddsScopeWhenSwitchIsTrue() + { + List itemsReceived = new List(); + + // Case where Scope is included. + IServiceProvider serviceProvider = ILoggerIntegrationTests.SetupApplicationInsightsLoggerIntegration( + (telemetryItem, telemetryProcessor) => itemsReceived.Add(telemetryItem), + configureTelemetryConfiguration: null, + configureApplicationInsightsOptions: (appInsightsLoggerOptions) => appInsightsLoggerOptions.IncludeScopes = true); + + ILogger testLogger = serviceProvider.GetRequiredService>(); + + using (testLogger.BeginScope("TestScope")) + { + using (testLogger.BeginScope>>(new Dictionary { { "Key", "Value" } })) + { + testLogger.LogInformation("Testing"); + testLogger.LogError(new Exception("TestException"), "Exception"); + } + } + + Assert.AreEqual(" => TestScope", (itemsReceived[0] as ISupportProperties).Properties["Scope"]); + Assert.AreEqual("Value", (itemsReceived[0] as ISupportProperties).Properties["Key"]); + + Assert.AreEqual(" => TestScope", (itemsReceived[1] as ISupportProperties).Properties["Scope"]); + Assert.AreEqual("Value", (itemsReceived[1] as ISupportProperties).Properties["Key"]); + + Assert.AreEqual("Testing", (itemsReceived[0] as TraceTelemetry).Message); + Assert.AreEqual("Exception", (itemsReceived[1] as ExceptionTelemetry).Message); + } + + /// + /// Ensures that the switch is honored and scopes are excluded + /// when switch is false. + /// + [TestMethod] + [TestCategory("ILogger")] + public void ApplicationInsightsLoggerDoesNotAddScopeWhenSwitchIsFalse() + { + List itemsReceived = new List(); + + // Case where Scope is NOT Included + IServiceProvider serviceProvider = ILoggerIntegrationTests.SetupApplicationInsightsLoggerIntegration( + (telemetryItem, telemetryProcessor) => itemsReceived.Add(telemetryItem), + configureTelemetryConfiguration: null, + configureApplicationInsightsOptions: (appInsightsLoggerOptions) => appInsightsLoggerOptions.IncludeScopes = false); + + ILogger testLogger = serviceProvider.GetRequiredService>(); + + using (testLogger.BeginScope("TestScope")) + { + using (testLogger.BeginScope>>(new Dictionary { { "Key", "Value" } })) + { + testLogger.LogInformation("Testing"); + testLogger.LogError(new Exception("TestException"), "Exception"); + } + } + + Assert.IsFalse((itemsReceived[0] as ISupportProperties).Properties.ContainsKey("Scope")); + Assert.IsFalse((itemsReceived[0] as ISupportProperties).Properties.ContainsKey("Key")); + + Assert.IsFalse((itemsReceived[1] as ISupportProperties).Properties.ContainsKey("Scope")); + Assert.IsFalse((itemsReceived[1] as ISupportProperties).Properties.ContainsKey("Key")); + + Assert.AreEqual("Testing", (itemsReceived[0] as TraceTelemetry).Message); + Assert.AreEqual("Exception", (itemsReceived[1] as ExceptionTelemetry).Message); + } + + /// + /// Test to ensure Instrumentation key is set correctly. + /// + [TestMethod] + [TestCategory("ILogger")] + public void ApplicationInsightsLoggerInstrumentationKeyIsSetCorrectly() + { + // Create DI container. + IServiceCollection services = new ServiceCollection(); + + services.AddLogging(loggingBuilder => + { + loggingBuilder.AddApplicationInsights("TestAIKey"); + }); + + TelemetryConfiguration telemetryConfiguration = services + .BuildServiceProvider() + .GetRequiredService>().Value; + + Assert.AreEqual("TestAIKey", telemetryConfiguration.InstrumentationKey); + } + + /// + /// Ensures that the default are as expected. + /// + [TestMethod] + [TestCategory("ILogger")] + public void DefaultLoggerOptionsAreCorrectlyRegistered() + { + IServiceProvider serviceProvider = ILoggerIntegrationTests.SetupApplicationInsightsLoggerIntegration( + (telemetryItem, telemetryProcessor) => { }); + + IOptions registeredOptions = + serviceProvider.GetRequiredService>(); + + Assert.IsTrue(registeredOptions.Value.TrackExceptionsAsExceptionTelemetry); + Assert.IsTrue(registeredOptions.Value.IncludeScopes); + } + + /// + /// Sets up the Application insights logger. + /// + /// Callback to execute when telemetry items are emitted. + /// Action to configure telemetry configuration. + /// Action to configure logger options. + /// Action to add, configure services to DI container. + /// Built DI container. + private static IServiceProvider SetupApplicationInsightsLoggerIntegration( + Action telemetryActionCallback, + Action configureTelemetryConfiguration = null, + Action configureApplicationInsightsOptions = null, + Func configureServices = null) + { + // Create DI container. + IServiceCollection services = new ServiceCollection(); + + // Configure the Telemetry configuration to be used to send data to AI. + services.Configure(telemetryConfiguration => + { + telemetryConfiguration.TelemetryProcessorChainBuilder.Use((existingProcessor) => + { + return new TestTelemetryProcessor(existingProcessor, telemetryActionCallback); + }).Build(); + }); + + if (configureTelemetryConfiguration != null) + { + services.Configure(configureTelemetryConfiguration); + } + + services.AddLogging(loggingBuilder => + { + if (configureApplicationInsightsOptions != null) + { + loggingBuilder.AddApplicationInsights(configureApplicationInsightsOptions); + } + else + { + loggingBuilder.AddApplicationInsights(); + } + + loggingBuilder.SetMinimumLevel(LogLevel.Trace); + }); + + if (configureServices != null) + { + services = configureServices.Invoke(services); + } + + IServiceProvider serviceProvider = services.BuildServiceProvider(); + + return serviceProvider; + } + } +} diff --git a/test/ILogger.NetStandard.Tests/TestTelemetryProcessor.cs b/test/ILogger.NetStandard.Tests/TestTelemetryProcessor.cs new file mode 100644 index 00000000..c4055ad4 --- /dev/null +++ b/test/ILogger.NetStandard.Tests/TestTelemetryProcessor.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.ApplicationInsights.Channel; +using Microsoft.ApplicationInsights.Extensibility; + +namespace Microsoft.ApplicationInsights +{ + /// + /// Test telemetry processor which gives access to the telemetry items as it passes through the pipeline. + /// + internal class TestTelemetryProcessor : ITelemetryProcessor + { + private readonly ITelemetryProcessor nextTelemetryProcessor; + private readonly Action telemetryActionCallback; + + /// + /// Initializes a new instances of the class. + /// + /// Next telemetry processor to invoke. + /// Action to invoke when the telemetry item is received. + public TestTelemetryProcessor(ITelemetryProcessor nextTelemetryProcessor, Action telemetryActionCallback) + { + this.nextTelemetryProcessor = nextTelemetryProcessor; + this.telemetryActionCallback = telemetryActionCallback; + } + + /// + /// Invokes the callback registered by the user. + /// + /// Telemetry item. + public void Process(ITelemetry item) + { + telemetryActionCallback.Invoke(item, this.nextTelemetryProcessor); + } + } +}