Skip to content
This repository has been archived by the owner on Oct 12, 2022. It is now read-only.

ILogger implementation for ApplicationInsights #239

Merged
merged 21 commits into from
Dec 31, 2018
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
fdd7589
ILogger implementation for ApplicationInsights
RamjotSingh Dec 7, 2018
7e21838
Adding one more tag
RamjotSingh Dec 7, 2018
d36c7b4
Taking PR comments
RamjotSingh Dec 17, 2018
2fcabd8
Taking PR comments
RamjotSingh Dec 17, 2018
a600dad
Using AI packages for 2.8.1
RamjotSingh Dec 18, 2018
1c1c736
Aligning the Options with the existing ConsoleLoggerOptions
RamjotSingh Dec 18, 2018
591ef6b
Removing IDisposable from AppInsightsLoggers and Provider no longer c…
RamjotSingh Dec 18, 2018
314972f
Renaming a file
RamjotSingh Dec 18, 2018
f9728ec
Adding unit tests and shifting from using TelemetryConfiguration to I…
RamjotSingh Dec 18, 2018
218c5c9
Removing uneeded flag.
RamjotSingh Dec 18, 2018
28f50a3
Adding extensions to specify Instrumentation key as part of logger re…
RamjotSingh Dec 18, 2018
615b150
PR comments and adding sourcelink support
RamjotSingh Dec 19, 2018
b1662ac
Taking PR comments
RamjotSingh Dec 20, 2018
14acccf
Moving back one version from latest SemVer version package since buil…
RamjotSingh Dec 21, 2018
75e0691
Moving Desktop analyzers one version back
RamjotSingh Dec 21, 2018
694b12e
Merge branch 'develop' into ramjsing/develop-ApplicationInsightsLogger
RamjotSingh Dec 21, 2018
1788dc2
TargetFrameworks -> TargetFramework
RamjotSingh Dec 21, 2018
919caa5
Merge branch 'ramjsing/develop-ApplicationInsightsLogger' of https://…
RamjotSingh Dec 21, 2018
299474b
Restoring to TargetFrameworks
RamjotSingh Dec 21, 2018
1eea0a3
Removing Analyzers
RamjotSingh Dec 21, 2018
701651e
Merge branch 'develop' into ramjsing/develop-ApplicationInsightsLogger
cijothomas Dec 31, 2018
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
11 changes: 11 additions & 0 deletions Logging.sln
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NLogTarget.NetCoreApp10.Tes
EndProject
RamjotSingh marked this conversation as resolved.
Show resolved Hide resolved
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Log4NetAppender.NetCoreApp10.Tests", "test\Log4NetAppender.NetCoreApp10.Tests\Log4NetAppender.NetCoreApp10.Tests.csproj", "{F74826DB-53B1-4588-BD02-4DD25DCBF292}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ILogger", "src\ILogger\ILogger.csproj", "{7D942802-E85E-4C3C-B97E-67A3867B7CBA}"
EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
test\CommonTestShared\CommonTestShared.projitems*{3b9ab7fa-562d-4e4e-86e3-3348426bc0d9}*SharedItemsImports = 13
Expand Down Expand Up @@ -225,6 +227,14 @@ 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
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -251,6 +261,7 @@ 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}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {AC8888B1-0E98-49D7-BE15-EB97A6261C0D}
Expand Down
214 changes: 214 additions & 0 deletions src/ILogger/ApplicationInsightsLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
// -----------------------------------------------------------------------
// <copyright file="ApplicationInsightsLogger.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation.
// All rights reserved. 2013
RamjotSingh marked this conversation as resolved.
Show resolved Hide resolved
// </copyright>
// -----------------------------------------------------------------------

namespace Microsoft.Extensions.Logging.ApplicationInsights
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.ApplicationInsights.Extensibility.Implementation;
using Microsoft.ApplicationInsights.Implementation;

/// <summary>
/// Application insights logger implementation for <see cref="ILogger"/>.
/// </summary>
/// <seealso cref="ILogger" />
public class ApplicationInsightsLogger : ILogger, IDisposable
RamjotSingh marked this conversation as resolved.
Show resolved Hide resolved
{
private readonly string categoryName;
private readonly TelemetryClient telemetryClient;
private readonly ApplicationInsightsLoggerOptions applicationInsightsLoggerOptions;

/// <summary>
/// Creates a new instance of <see cref="ApplicationInsightsLogger"/>.
/// </summary>
public ApplicationInsightsLogger(
string categoryName,
TelemetryClient telemetryClient,
ApplicationInsightsLoggerOptions applicationInsightsLoggerOptions)
{
this.categoryName = categoryName;
this.telemetryClient = telemetryClient;
this.telemetryClient.Context.GetInternalContext().SdkVersion = SdkVersionUtils.GetSdkVersion("il:");
this.applicationInsightsLoggerOptions = applicationInsightsLoggerOptions ?? throw new ArgumentNullException(nameof(applicationInsightsLoggerOptions));
}

/// <summary>
/// Gets or sets the external scope provider.
/// </summary>
internal IExternalScopeProvider ExternalScopeProvider { get; set; }

/// <summary>
/// Begins a logical operation scope.
/// </summary>
/// <typeparam name="TState">Current state.</typeparam>
/// <param name="state">The identifier for the scope.</param>
/// <returns>
/// An IDisposable that ends the logical operation scope on dispose.
/// </returns>
public IDisposable BeginScope<TState>(TState state)
{
return this.ExternalScopeProvider != null ? this.ExternalScopeProvider.Push(state) : NullScope.Instance;
RamjotSingh marked this conversation as resolved.
Show resolved Hide resolved
}

/// <summary>
/// Checks if the given <paramref name="logLevel" /> is enabled.
/// </summary>
/// <param name="logLevel">level to be checked.</param>
/// <returns>
/// <c>true</c> if enabled.
/// </returns>
public bool IsEnabled(LogLevel logLevel)
{
return this.telemetryClient.IsEnabled();
RamjotSingh marked this conversation as resolved.
Show resolved Hide resolved
}

/// <summary>
/// Writes a log entry.
/// </summary>
/// <typeparam name="TState">State being passed along.</typeparam>
/// <param name="logLevel">Entry will be written on this level.</param>
/// <param name="eventId">Id of the event.</param>
/// <param name="state">The entry to be written. Can be also an object.</param>
/// <param name="exception">The exception related to this entry.</param>
/// <param name="formatter">Function to create a <c>string</c> message of the <paramref name="state" /> and <paramref name="exception" />.</param>
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> 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),
};
exceptionTelemetry.Context.Properties["Exception"] = exception.ToString();
RamjotSingh marked this conversation as resolved.
Show resolved Hide resolved
this.PopulateTelemetry(exceptionTelemetry, state, eventId);
this.telemetryClient.TrackException(exceptionTelemetry);
}
}
}

/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}

/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool disposing)
{
}

/// <summary>
/// Converts the <see cref="LogLevel"/> into corresponding Application insights <see cref="SeverityLevel"/>.
/// </summary>
/// <param name="logLevel">Logging log level.</param>
/// <returns>Application insights corresponding SeverityLevel for the LogLevel.</returns>
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;
}
}

/// <summary>
/// Populates the state, scope and event information for the logging event.
/// </summary>
/// <typeparam name="TState">State information for the current event.</typeparam>
/// <param name="telemetry">Telemetry item.</param>
/// <param name="state">Event state information.</param>
/// <param name="eventId">Event Id information.</param>
private void PopulateTelemetry<TState>(ITelemetry telemetry, TState state, EventId eventId)
{
IDictionary<string, string> dict = telemetry.Context.Properties;
RamjotSingh marked this conversation as resolved.
Show resolved Hide resolved
dict["CategoryName"] = this.categoryName;

if (this.applicationInsightsLoggerOptions.IncludeEventId)
{
if (eventId.Id != 0)
{
dict["EventId"] = eventId.Id.ToString(System.Globalization.CultureInfo.InvariantCulture);
}

if (!string.IsNullOrEmpty(eventId.Name))
{
dict["EventName"] = eventId.Name;
}
}

if (state is IReadOnlyList<KeyValuePair<string, object>> stateDictionary)
{
foreach (KeyValuePair<string, object> 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<KeyValuePair<string, object>>.
// 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 IReadOnlyList<KeyValuePair<string, object>> activeScopeDictionary)
{
foreach (KeyValuePair<string, object> item in activeScopeDictionary)
{
dict[item.Key] = Convert.ToString(item.Value, CultureInfo.InvariantCulture);
}
}
else
{
stringBuilder.Append(" => ").Append(activeScope);
RamjotSingh marked this conversation as resolved.
Show resolved Hide resolved
}
},
stringBuilder);

if (stringBuilder.Length > 0)
{
dict["Scope"] = stringBuilder.ToString();
}
}
}
}
}
54 changes: 54 additions & 0 deletions src/ILogger/ApplicationInsightsLoggerExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// -----------------------------------------------------------------------
// <copyright file="ApplicationInsightsLoggerExtensions.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation.
// All rights reserved. 2013
// </copyright>
// -----------------------------------------------------------------------

namespace Microsoft.Extensions.Logging
{
using System;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging.ApplicationInsights;

/// <summary>
/// Extensions methods to add and configure application insights logger.
/// </summary>
public static class ApplicationInsightsLoggerExtensions
RamjotSingh marked this conversation as resolved.
Show resolved Hide resolved
{
/// <summary>
/// Adds an ApplicationInsights logger named 'ApplicationInsights' to the factory.
/// </summary>
/// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param>
public static ILoggingBuilder AddApplicationInsights(this ILoggingBuilder builder)
{
return builder.AddApplicationInsights((applicationInsightsOptions) => { });
RamjotSingh marked this conversation as resolved.
Show resolved Hide resolved
}

/// <summary>
/// Adds an ApplicationInsights logger named 'ApplicationInsights' to the factory.
/// </summary>
/// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param>
/// <param name="configureApplicationInsightsOptions">Action to configure ApplicationInsights logger.</param>
public static ILoggingBuilder AddApplicationInsights(
this ILoggingBuilder builder,
Action<ApplicationInsightsLoggerOptions> configureApplicationInsightsOptions)
{
if (configureApplicationInsightsOptions == null)
{
throw new ArgumentNullException(nameof(configureApplicationInsightsOptions));
}

// This ensures that we don't override the TelemetryClient if one was added using .AddApplicationInsightsTelemetry()
// However if one was not added at the time of calling LoggerProvider, use a dummy one so that logger does not throw.
// .AddApplicationInsightsTelemetry() will add a configured TelemetryClient which will then override this one.
builder.Services.TryAdd(ServiceDescriptor.Singleton<TelemetryConfiguration, TelemetryConfiguration>());
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, ApplicationInsightsLoggerProvider>());
builder.Services.Configure(configureApplicationInsightsOptions);

return builder;
}
}
}
37 changes: 37 additions & 0 deletions src/ILogger/ApplicationInsightsLoggerOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// -----------------------------------------------------------------------
// <copyright file="ApplicationInsightsLoggerOptions.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation.
// All rights reserved. 2013
// </copyright>
// -----------------------------------------------------------------------

namespace Microsoft.Extensions.Logging.ApplicationInsights
{
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.Extensions.Logging;

/// <summary>
/// <see cref="ApplicationInsightsLoggerOptions"/> defines the custom behavior of the tracing information sent to Application Insights.
/// </summary>
public class ApplicationInsightsLoggerOptions
{
/// <summary>
/// Initializes a new instance of the <see cref="ApplicationInsightsLoggerOptions" /> class.
/// Application Insights logger options can configure how <see cref="ILogger"/> behaves when sending telemetry.
/// </summary>
public ApplicationInsightsLoggerOptions()
{
this.TrackExceptionsAsExceptionTelemetry = true;
}

/// <summary>
/// Gets or sets a value indicating whether to track exceptions as <see cref="ExceptionTelemetry"/>.
/// </summary>
public bool TrackExceptionsAsExceptionTelemetry { get; set; }

/// <summary>
/// Gets or sets a value indicating whether EventId and EventName properties should be included in telemetry.
/// </summary>
public bool IncludeEventId { get; set; }
RamjotSingh marked this conversation as resolved.
Show resolved Hide resolved
}
}
Loading