diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0434444 --- /dev/null +++ b/.gitignore @@ -0,0 +1,256 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +/src/Tests/coverage.json +/src/Tests/coverage.opencover.xml +/src/ConsoleApp1 +/src/WebApplication1 diff --git a/README.md b/README.md new file mode 100644 index 0000000..6da9e30 --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# **Cloud.Core.Telemetry.AzureAppInsights** + +
+ +Azure specific implementation of telemetry logging. Uses the ITelemeteryLogger interface from _Cloud.Core_ and can be used in conjunction with the Telemetry Logging +Provider classes found in the _Cloud.Core.Telemetry.Logging_ package. + +
+ +## Usage + +As the logger follows the base `ILogger` implementation, it's very flexible in how it can be used. To create an instance, do the following: + +``` +var logger = new AppInsightsLogger("insightsKey"); + +logger.LogInformation("Sample information message"); +logger.LogWarning("Sample warning message"); +logger.LogDebug("Sample debug message"); +logger.LogException("Sample exception message"); +``` + +Any of the logging methods can also handle exception, such as: + +``` +logger.LogWarning(new Exception("Something's gone wrong!")); +`` + +## Test Coverage +A threshold will be added to this package to ensure the test coverage is above 80% for branches, functions and lines. If it's not above the required threshold +(threshold that will be implemented on ALL of the new core repositories going forward), then the build will fail. + +## Compatibility +This package has has been written in .net Standard and can be therefore be referenced from a .net Core or .net Framework application. The advantage of utilising from a .net Core application, +is that it can be deployed and run on a number of host operating systems, such as Windows, Linux or OSX. Unlike referencing from the a .net Framework application, which can only run on +Windows (or Linux using Mono). + +## Setup +This package requires the .net Core 2.1 SDK, it can be downloaded here: +https://www.microsoft.com/net/download/dotnet-core/2.1 + +IDE of Visual Studio or Visual Studio Code, can be downloaded here: +https://visualstudio.microsoft.com/downloads/ + +## How to access this package +All of the Cloud.Core.* packages are published to our internal NuGet feed. To consume this on your local development machine, please add the following feed to your feed sources in Visual Studio: +TBC + +For help setting up, follow this article: https://docs.microsoft.com/en-us/vsts/package/nuget/consume?view=vsts diff --git a/src/Cloud.Core.Telemetry.AzureAppInsights.sln b/src/Cloud.Core.Telemetry.AzureAppInsights.sln new file mode 100644 index 0000000..1144336 --- /dev/null +++ b/src/Cloud.Core.Telemetry.AzureAppInsights.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27703.2047 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cloud.Core.Telemetry.AzureAppInsights", "Cloud.Core.Telemetry.AzureAppInsights\Cloud.Core.Telemetry.AzureAppInsights.csproj", "{49353CB1-6857-4E9E-B65C-A9A894025902}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cloud.Core.Telemetry.AzureAppInsights.Tests", "Tests\Cloud.Core.Telemetry.AzureAppInsights.Tests.csproj", "{F0CD7CC0-BDAB-4AA8-8C5B-0D5829C6820F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {49353CB1-6857-4E9E-B65C-A9A894025902}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {49353CB1-6857-4E9E-B65C-A9A894025902}.Debug|Any CPU.Build.0 = Debug|Any CPU + {49353CB1-6857-4E9E-B65C-A9A894025902}.Release|Any CPU.ActiveCfg = Release|Any CPU + {49353CB1-6857-4E9E-B65C-A9A894025902}.Release|Any CPU.Build.0 = Release|Any CPU + {F0CD7CC0-BDAB-4AA8-8C5B-0D5829C6820F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F0CD7CC0-BDAB-4AA8-8C5B-0D5829C6820F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F0CD7CC0-BDAB-4AA8-8C5B-0D5829C6820F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F0CD7CC0-BDAB-4AA8-8C5B-0D5829C6820F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D03DCC35-27ED-48CE-ADFA-5D0537FB20FF} + EndGlobalSection +EndGlobal diff --git a/src/Cloud.Core.Telemetry.AzureAppInsights.sln.DotSettings b/src/Cloud.Core.Telemetry.AzureAppInsights.sln.DotSettings new file mode 100644 index 0000000..cb1f1a6 --- /dev/null +++ b/src/Cloud.Core.Telemetry.AzureAppInsights.sln.DotSettings @@ -0,0 +1,22 @@ + + SUGGESTION + SUGGESTION + SUGGESTION + SUGGESTION + SUGGESTION + True + DO_NOT_SHOW + SUGGESTION + SUGGESTION + SUGGESTION + SUGGESTION + SUGGESTION + <Configurator><ConnectList /></Configurator> + True + + True + 1 + True + True + + \ No newline at end of file diff --git a/src/Cloud.Core.Telemetry.AzureAppInsights/AppInsights.cs b/src/Cloud.Core.Telemetry.AzureAppInsights/AppInsights.cs new file mode 100644 index 0000000..52e3061 --- /dev/null +++ b/src/Cloud.Core.Telemetry.AzureAppInsights/AppInsights.cs @@ -0,0 +1,361 @@ +namespace Cloud.Core.Telemetry.AzureAppInsights +{ + using Diagnostics = System.Diagnostics; + using System; + using Microsoft.Extensions.Logging; + using System.Collections.Generic; + using Microsoft.ApplicationInsights.DataContracts; + using Microsoft.ApplicationInsights.Extensibility; + using Microsoft.ApplicationInsights; + using System.Runtime.CompilerServices; + + /// + /// Azure specific Application Insights implementation. + /// + /// + public class AppInsightsLogger : ITelemetryLogger + { + private readonly string _instrumentationKey; + private readonly LogLevel _level; + + private TelemetryClient _client; + + public string InstrumentationKey => _instrumentationKey; + public LogLevel LogLevel => _level; + + internal TelemetryClient Client + { + get + { + if (_client == null) + { + // Initialise client. + TelemetryConfiguration.Active.InstrumentationKey = _instrumentationKey; + _client = new TelemetryClient(); + + // Default application information - who's running the code, guid for session id and operating system being executed on. + Client.Context.User.Id = AppDomain.CurrentDomain.FriendlyName; + Client.Context.Session.Id = Guid.NewGuid().ToString(); + Client.Context.Device.OperatingSystem = Environment.OSVersion.ToString(); + } + + return _client; + } + } + + /// + /// Initializes a new instance of the class. + /// + public AppInsightsLogger(string instrumentationKey, LogLevel level) + { + _instrumentationKey = instrumentationKey; + _level = level; + } + + /// + public void Flush() + { + Client.Flush(); + } + + /// + /// Checks if the given is enabled. + /// + /// level to be checked. + /// + /// true if enabled. + /// + public bool IsEnabled(LogLevel logLevel) + { + return logLevel >= _level; + } + + /// + /// Begins the scope. + /// + /// + /// The state. + /// + public IDisposable BeginScope(T state) + { + return null; + } + + /// + public void Log(LogLevel logLevel, EventId eventId, T state, Exception exception, + Func formatter) + { + if (!IsEnabled(logLevel)) + return; + + var message = formatter != null ? formatter(state, exception) : state.ToString(); + + if (string.IsNullOrEmpty(eventId.Name)) + { + var frame = new Diagnostics.StackFrame(5); // Default stack level which equates to the actual calling code... + + var method = frame.GetMethod(); + + if (method.IsNullOrDefault()) + { + frame = new Diagnostics.StackFrame(4); + method = frame.GetMethod(); + } + + var type = method.DeclaringType; + var name = method.Name; + + eventId = new EventId( + eventId.Id == 0 ? BitConverter.ToInt32(Guid.NewGuid().ToByteArray(), 0) : eventId.Id, + $"{type}:{name}"); + } + + var props = new Dictionary + { + {"Telemetry.EventId", eventId.Id.ToString()}, + {"Telemetry.EventName", eventId.Name} + }; + + if (exception != null) + { + CreateTelemetryException(logLevel, exception, props, string.Empty, string.Empty, -1); + } + else + { + CreateTelemetryEvent(logLevel, message, props, string.Empty, string.Empty, -1); + } + } + + /// + public void LogVerbose(string message, Dictionary properties = null, + [CallerMemberName] string callerMemberName = "", [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = -1) + { + CreateTelemetryEvent(LogLevel.Information, message, properties, callerMemberName, callerFilePath, + callerLineNumber); + } + + /// + public void LogInformation(string message, Dictionary properties = null, + [CallerMemberName] string callerMemberName = "", [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = -1) + { + CreateTelemetryEvent(LogLevel.Information, message, properties, callerMemberName, callerFilePath, + callerLineNumber); + } + + /// + public void LogCritical(string message, Dictionary properties = null, + [CallerMemberName] string callerMemberName = "", [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = -1) + { + CreateTelemetryEvent(LogLevel.Information, message, properties, callerMemberName, callerFilePath, + callerLineNumber); + } + + /// + public void LogCritical(Exception ex, Dictionary properties = null, + [CallerMemberName] string callerMemberName = "", [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = -1) + { + CreateTelemetryException(LogLevel.Critical, ex, properties, callerMemberName, callerFilePath, + callerLineNumber); + } + + /// Logs the debug. + /// The message. + /// The properties. + /// Name of the caller member. + /// The caller file path. + /// The caller line number. + public void LogDebug(string message, Dictionary properties = null, + [CallerMemberName] string callerMemberName = "", [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = -1) + { + CreateTelemetryEvent(LogLevel.Debug, message, properties, callerMemberName, callerFilePath, + callerLineNumber); + } + + /// Logs the debug. + /// The ex. + /// The properties. + /// Name of the caller member. + /// The caller file path. + /// The caller line number. + public void LogDebug(Exception ex, Dictionary properties = null, + [CallerMemberName] string callerMemberName = "", [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = -1) + { + CreateTelemetryException(LogLevel.Debug, ex, properties, callerMemberName, callerFilePath, + callerLineNumber); + } + + /// + public void LogWarning(string message, Dictionary properties = null, + [CallerMemberName] string callerMemberName = "", [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = -1) + { + CreateTelemetryEvent(LogLevel.Warning, message, properties, callerMemberName, callerFilePath, + callerLineNumber); + } + + /// + public void LogWarning(Exception ex, Dictionary properties = null, + [CallerMemberName] string callerMemberName = "", [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = -1) + { + CreateTelemetryException(LogLevel.Warning, ex, properties, callerMemberName, callerFilePath, + callerLineNumber); + } + + /// + public void LogError(string message, Dictionary properties = null, + [CallerMemberName] string callerMemberName = "", [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = -1) + { + CreateTelemetryEvent(LogLevel.Error, message, properties, callerMemberName, callerFilePath, + callerLineNumber); + } + + /// + public void LogError(Exception ex, Dictionary properties = null, + [CallerMemberName] string callerMemberName = "", [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = -1) + { + CreateTelemetryException(LogLevel.Error, ex, properties, callerMemberName, callerFilePath, + callerLineNumber); + } + + /// + public void LogMetric(string metricName, double metricValue, + [CallerMemberName] string callerMemberName = "", [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = -1) + { + CreateTelemetryMetric(LogLevel.Information, metricName, metricValue, null, callerMemberName, callerFilePath, + callerLineNumber); + } + + /// + /// Sets the default properties. + /// + /// The log level. + /// The properties. + /// Name of the caller member. + /// The caller file path. + /// The caller line number. + /// + private Dictionary SetDefaultProperties(LogLevel logLevel, + Dictionary properties, + string callerMemberName, string callerFilePath, int callerLineNumber) + { + // If properties were not set, initialise now and add default properties. + var output = properties == null ? new Dictionary() : new Dictionary(properties); + + output.Add("Telemetry.LogLevel", logLevel.ToString()); + + if (!string.IsNullOrEmpty(callerMemberName) && !output.ContainsKey("Telemetry.SummaryMessage")) + output.Add("Telemetry.MemberName", callerMemberName); + + if (!string.IsNullOrEmpty(callerFilePath) && !output.ContainsKey("Telemetry.SummaryMessage")) + output.Add("Telemetry.FilePath", System.IO.Path.GetFileName(callerFilePath)); + + if (callerLineNumber > 0 && !output.ContainsKey("Telemetry.SummaryMessage")) + output.Add("Telemetry.LineNumber", callerLineNumber.ToString()); + + return output; + } + + /// Creates the telemetry event. + /// The level. + /// The message. + /// The properties. + /// Name of the caller member. + /// The caller file path. + /// The caller line number. + private void CreateTelemetryEvent(LogLevel level, string message, Dictionary properties, + string callerMemberName, string callerFilePath, int callerLineNumber) + { + if (!IsEnabled(level)) + return; + + var telemetry = new EventTelemetry(message); + + var output = SetDefaultProperties(level, properties, callerMemberName, callerFilePath, callerLineNumber); + + if (!string.IsNullOrEmpty(message) && !telemetry.Properties.ContainsKey("Telemetry.SummaryMessage")) + telemetry.Properties.Add("Telemetry.SummaryMessage", message); + + foreach (var property in output) + { + if (!telemetry.Properties.ContainsKey(property.Key)) + telemetry.Properties.Add(property); + } + + Client.TrackEvent(telemetry); + } + + /// + /// Creates the telemetry exception. + /// + /// The level. + /// The ex. + /// The properties. + /// Name of the caller member. + /// The caller file path. + /// The caller line number. + private void CreateTelemetryException(LogLevel level, Exception ex, Dictionary properties, + string callerMemberName, string callerFilePath, int callerLineNumber) + { + if (!IsEnabled(level)) + return; + + var telemetry = new ExceptionTelemetry(ex); + + var output = SetDefaultProperties(level, properties, callerMemberName, callerFilePath, callerLineNumber); + + if (!string.IsNullOrEmpty(ex.Message) && !telemetry.Properties.ContainsKey("Telemetry.ExceptionMessage")) + telemetry.Properties.Add("Telemetry.ExceptionMessage", ex.Message); + + foreach (var property in output) + { + if (!telemetry.Properties.ContainsKey(property.Key)) + telemetry.Properties.Add(property); + } + + Client.TrackException(telemetry); + } + + /// + /// Creates the telemetry metric. + /// + /// The level. + /// Name of the metric. + /// The metric value. + /// The properties. + /// Name of the caller member. + /// The caller file path. + /// The caller line number. + private void CreateTelemetryMetric(LogLevel level, string metricName, double metricValue, + Dictionary properties, + string callerMemberName, string callerFilePath, int callerLineNumber) + { + if (!IsEnabled(level)) + return; + + var telemetry = new MetricTelemetry(metricName, metricValue); + + var output = SetDefaultProperties(level, properties, callerMemberName, callerFilePath, callerLineNumber); + + if (!string.IsNullOrEmpty(metricName) && !telemetry.Properties.ContainsKey("Telemetry.Metric")) + telemetry.Properties.Add("Telemetry.Metric", metricName); + + foreach (var property in output) + { + if (!telemetry.Properties.ContainsKey(property.Key)) + telemetry.Properties.Add(property); + } + + Client.TrackMetric(telemetry); + } + } +} \ No newline at end of file diff --git a/src/Cloud.Core.Telemetry.AzureAppInsights/AssemblyInfo.cs b/src/Cloud.Core.Telemetry.AzureAppInsights/AssemblyInfo.cs new file mode 100644 index 0000000..2ec5797 --- /dev/null +++ b/src/Cloud.Core.Telemetry.AzureAppInsights/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Cloud.Core.Telemetry.AzureAppInsights.Tests")] +[assembly: InternalsVisibleTo("Cloud.Core.Telemetry.AzureAppInsights.Tests.Profiler")] \ No newline at end of file diff --git a/src/Cloud.Core.Telemetry.AzureAppInsights/Cloud.Core.Telemetry.AzureAppInsights.csproj b/src/Cloud.Core.Telemetry.AzureAppInsights/Cloud.Core.Telemetry.AzureAppInsights.csproj new file mode 100644 index 0000000..b11801a --- /dev/null +++ b/src/Cloud.Core.Telemetry.AzureAppInsights/Cloud.Core.Telemetry.AzureAppInsights.csproj @@ -0,0 +1,38 @@ + + + + netstandard2.0 + Robert McCabe + true + Cloud.Core.Telemetry.AzureAppInsights + false + Cloud.Core.Telemetry.AzureAppInsights AppInsights Azure .net-Core Core Logging + + Azure specific implementation of ITelemetryLogger using AppInsights. + Cloud.Core.Telemetry.AzureAppInsights + 1.0 + Robert McCabe + + + + + + + + + + + + + ..\..\..\..\..\.nuget\packages\newtonsoft.json\11.0.2\lib\netstandard2.0\Newtonsoft.Json.dll + + + ..\..\..\..\..\..\..\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7\System.Configuration.dll + + + + + + + + diff --git a/src/Cloud.Core.Telemetry.AzureAppInsights/Extensions/LoggingBuilderExtensions.cs b/src/Cloud.Core.Telemetry.AzureAppInsights/Extensions/LoggingBuilderExtensions.cs new file mode 100644 index 0000000..17eba81 --- /dev/null +++ b/src/Cloud.Core.Telemetry.AzureAppInsights/Extensions/LoggingBuilderExtensions.cs @@ -0,0 +1,54 @@ +namespace Microsoft.Extensions.Logging +{ + using System; + using Cloud.Core; + using Cloud.Core.Telemetry.AzureAppInsights; + using Cloud.Core.Telemetry.Logging; + using Microsoft.Extensions.Configuration; + using DependencyInjection; + + /// + /// Telemetry logger factory adds the AddTelemetryLogger method to the ILoggingBuilder. + /// + public static class LoggingBuilderExtensions + { + /// + /// Adds the telemetry logger factory method to the logging builder. + /// + /// The builder to add to. + /// The modified builder. + public static ILoggingBuilder AddAppInsightsLogger(this ILoggingBuilder builder) + { + builder.Services.AddSingleton((a) => + { + var config = a.GetService(); + + var key = config.GetValue("Logging:InstrumentationKey"); + var defaultLevel = config.GetValue("Logging:LogLevel:Default"); + var telemetryLevel = config.GetValue("Logging:LogLevel:Telemetry"); + + return new AppInsightsLogger(key, GetLogLevel(defaultLevel, telemetryLevel)); + }); + + builder.Services.AddSingleton(a => new TelemetryLoggerProvider(a.GetService())); + return builder; + } + + private static LogLevel GetLogLevel(string defaultLevel, string desiredLevel) + { + if (!desiredLevel.IsNullOrEmpty()) + defaultLevel = desiredLevel; + + switch (defaultLevel) + { + case "Trace": return LogLevel.Trace; + case "Information": return LogLevel.Information; + case "Critical": return LogLevel.Critical; + case "Debug": return LogLevel.Debug; + case "Error": return LogLevel.Error; + case "Warning": return LogLevel.Warning; + default: return LogLevel.None; + } + } + } +} \ No newline at end of file diff --git a/src/Tests/AppInsightsTest.cs b/src/Tests/AppInsightsTest.cs new file mode 100644 index 0000000..096ec53 --- /dev/null +++ b/src/Tests/AppInsightsTest.cs @@ -0,0 +1,450 @@ +using System; +using System.Collections.Generic; +using Cloud.Core.Testing; +using FluentAssertions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace Cloud.Core.Telemetry.AzureAppInsights.Tests.Unit +{ + public class AppInsightsTest + { + [Fact, IsUnit] + public void Test_Telemetry_IsEnabled() + { + var logger = new AppInsightsLogger("test", LogLevel.Trace); + logger.BeginScope("test"); + logger.Flush(); + Assert.True(logger.IsEnabled(LogLevel.Information)); + } + + [Fact, IsUnit] + public void Test_Telemetry_LogWarningMessage() + { + AssertExtensions.DoesNotThrow(() => + { + var logger = new AppInsightsLogger("test", LogLevel.Trace); + logger.LogWarning("test"); + logger.Flush(); + }); + } + + [Fact, IsUnit] + public void Test_Telemetry_LogWithWrongLevel() + { + AssertExtensions.DoesNotThrow(() => + { + var logger = new AppInsightsLogger("test", LogLevel.Warning); + logger.LogDebug("test"); + logger.Flush(); + }); + } + + [Fact, IsUnit] + public void Test_Telemetry_LogCriticalMessage() + { + AssertExtensions.DoesNotThrow(() => + { + var logger = new AppInsightsLogger("test", LogLevel.Trace); + logger.LogCritical("test"); + logger.Flush(); + }); + } + + [Fact, IsUnit] + public void Test_Telemetry_LogInformationMessage() + { + AssertExtensions.DoesNotThrow(() => + { + var logger = new AppInsightsLogger("test", LogLevel.Trace); + + + Dictionary statistics = new Dictionary() + { + {"SomeProp1", "sample1"}, + {"SomeProp2", "sample2"}, + {"SomeProp3", "sample3" } + }; + + logger.LogInformation("test title", statistics); + logger.Log(LogLevel.Information, new EventId(1, null), "A", null, (a, b) => + { + return string.Format("{0}:{1}", a, b); + }); + logger.Flush(); + }); + } + + [Fact, IsUnit] + public void Test_Telemetry_LogVerboseMessage() + { + AssertExtensions.DoesNotThrow(() => + { + var logger = new AppInsightsLogger("test", LogLevel.Trace); + logger.LogVerbose("test"); + logger.Flush(); + }); + } + + [Fact, IsUnit] + public void Test_Telemetry_LogErrorMessage() + { + AssertExtensions.DoesNotThrow(() => + { + var logger = new AppInsightsLogger("test", LogLevel.Trace); + logger.LogError("test"); + logger.Flush(); + }); + } + + [Fact, IsUnit] + public void Test_Telemetry_LogWarningDebug() + { + AssertExtensions.DoesNotThrow(() => + { + var logger = new AppInsightsLogger("test", LogLevel.Trace); + logger.LogDebug(new Exception()); + logger.Flush(); + }); + } + + [Fact, IsUnit] + public void Test_Telemetry_LogWarningException() + { + AssertExtensions.DoesNotThrow(() => + { + var logger = new AppInsightsLogger("test", LogLevel.Trace); + logger.LogWarning(new Exception()); + logger.Flush(); + }); + } + + [Fact, IsUnit] + public void Test_Telemetry_LoCriticalException() + { + AssertExtensions.DoesNotThrow(() => + { + var logger = new AppInsightsLogger("test", LogLevel.Trace); + logger.LogCritical(new Exception()); + logger.Flush(); + }); + } + + [Fact, IsUnit] + public void Test_Telemetry_LogExceptionWithMessage() + { + AssertExtensions.DoesNotThrow(() => + { + var logger = new AppInsightsLogger("test", LogLevel.Trace); + logger.LogError(new Exception(), "Some error"); + logger.Flush(); + }); + } + + [Fact, IsUnit] + public void Test_Telemetry_ExceptionWithMessageObject() + { + AssertExtensions.DoesNotThrow(() => + { + var logger = new AppInsightsLogger("test", LogLevel.Trace); + logger.LogError(new Exception(), "test", new Dictionary { { "a", "a" } }); + logger.Flush(); + }); + } + + [Fact, IsUnit] + public void Test_Telemetry_ExceptionWithMessageFullParams() + { + AssertExtensions.DoesNotThrow(() => + { + var logger = new AppInsightsLogger("test", LogLevel.Trace); + logger.LogError(new Exception(), "test", new Dictionary { { "a", "a" } }, "", "", 1); + logger.Flush(); + }); + } + + [Fact, IsUnit] + public void Test_Telemetry_ExceptionWithDictionary() + { + AssertExtensions.DoesNotThrow(() => + { + var logger = new AppInsightsLogger("test", LogLevel.Trace); + logger.LogError(new Exception(), new Dictionary { { "a", "a" } }); + logger.Flush(); + }); + } + + [Fact, IsUnit] + public void Test_Telemetry_LogLevelDebug() + { + AssertExtensions.DoesNotThrow(() => + { + var logger = new AppInsightsLogger("test", LogLevel.Debug); + logger.Log(LogLevel.Debug, new EventId(1, "test"), "test", new Exception("test"), null); + logger.Flush(); + }); + } + + [Fact, IsUnit] + public void Test_Telemetry_LogLevelError() + { + AssertExtensions.DoesNotThrow(() => + { + var logger = new AppInsightsLogger("test", LogLevel.Error); + logger.Log(LogLevel.Error, new EventId(1, "test"), "test", new Exception("test"), null); + logger.Flush(); + }); + } + + [Fact, IsUnit] + public void Test_Telemetry_LogLevelInfo() + { + AssertExtensions.DoesNotThrow(() => + { + var logger = new AppInsightsLogger("test", LogLevel.Information); + logger.Log(LogLevel.Information, new EventId(1, "test"), "test", null, null); + logger.Flush(); + }); + } + + [Fact, IsUnit] + public void Test_Telemetry_LogLevelInfoWithException() + { + AssertExtensions.DoesNotThrow(() => + { + var logger = new AppInsightsLogger("test", LogLevel.Information); + logger.Log(LogLevel.Error, new EventId(1, "test"), "test", new Exception("test"), null); + logger.Flush(); + }); + } + + [Fact, IsUnit] + public void Test_Telemetry_LogLevelInfoWithEvent() + { + AssertExtensions.DoesNotThrow(() => + { + var logger = new AppInsightsLogger("test", LogLevel.Information); + logger.Log(LogLevel.Information, new EventId(1, "test"), "test", null, null); + logger.Flush(); + }); + } + + [Fact, IsUnit] + public void Test_Telemetry_LogLevelNoneWithException() + { + AssertExtensions.DoesNotThrow(() => + { + var logger = new AppInsightsLogger("test", LogLevel.None); + logger.Log(LogLevel.Trace, new EventId(1, "test"), "test", new Exception(), null); + logger.Flush(); + }); + } + + [Fact, IsUnit] + public void Test_Telemetry_LogLevelNone() + { + AssertExtensions.DoesNotThrow(() => + { + var logger = new AppInsightsLogger("test", LogLevel.None); + logger.Log(LogLevel.Trace, new EventId(1, "test"), "test", null, null); + logger.LogError(new Exception(), null, null); + logger.LogMetric("test", Double.MinValue); + logger.Log(LogLevel.Trace, new EventId(1, "test"), "test", null, null); + logger.Flush(); + }); + } + + [Fact, IsUnit] + public void Test_Telemetry_LogMetric() + { + AssertExtensions.DoesNotThrow(() => + { + var logger = new AppInsightsLogger("test", LogLevel.Trace); + logger.LogMetric("test", 3.1); + logger.Flush(); + }); + } + + [Fact, IsUnit] + public void Test_BuilderExtension_Critical() + { + IConfiguration config = new ConfigurationBuilder().AddInMemoryCollection(new List>() + { + new KeyValuePair("Logging:Instrumentationkey", "test"), + new KeyValuePair("Logging:LogLevel:Default", "Critical") + + }).Build(); + + IServiceCollection collection = new ServiceCollection() + .AddSingleton(config) + .AddLogging(builder => + { + builder.AddConfiguration(config); + builder.AddAppInsightsLogger(); + }); + + var prov = collection.BuildServiceProvider(); + + var logger = prov.GetService() as AppInsightsLogger; + var logProvider = prov.GetService(); + + logProvider.Should().NotBeNull(); + logger.Should().NotBeNull(); + logger.LogLevel.Should().Be(LogLevel.Critical); + logger.InstrumentationKey.Should().Be("test"); + } + + [Fact, IsUnit] + public void Test_BuilderExtension_Debug() + { + IConfiguration config = new ConfigurationBuilder().AddInMemoryCollection(new List>() + { + new KeyValuePair("Logging:Instrumentationkey", "test"), + new KeyValuePair("Logging:LogLevel:Default", "Information"), + new KeyValuePair("Logging:LogLevel:Telemetry", "Debug"), + + }).Build(); + + IServiceCollection collection = new ServiceCollection() + .AddSingleton(config) + .AddLogging(builder => + { + builder.AddConfiguration(config); + builder.AddAppInsightsLogger(); + }); + + var logger = collection.BuildServiceProvider().GetService() as AppInsightsLogger; + + logger.Should().NotBeNull(); + logger.LogLevel.Should().Be(LogLevel.Debug); + logger.InstrumentationKey.Should().Be("test"); + } + + [Fact, IsUnit] + public void Test_BuilderExtension_Information() + { + IConfiguration config = new ConfigurationBuilder().AddInMemoryCollection(new List>() + { + new KeyValuePair("Logging:Instrumentationkey", "test"), + new KeyValuePair("Logging:LogLevel:Default", "Information"), + + }).Build(); + + IServiceCollection collection = new ServiceCollection() + .AddSingleton(config) + .AddLogging(builder => + { + builder.AddConfiguration(config); + builder.AddAppInsightsLogger(); + }); + + var logger = collection.BuildServiceProvider().GetService() as AppInsightsLogger; + + logger.Should().NotBeNull(); + logger.LogLevel.Should().Be(LogLevel.Information); + logger.InstrumentationKey.Should().Be("test"); + } + + [Fact, IsUnit] + public void Test_BuilderExtension_Warning() + { + IConfiguration config = new ConfigurationBuilder().AddInMemoryCollection(new List>() + { + new KeyValuePair("Logging:Instrumentationkey", "test"), + new KeyValuePair("Logging:LogLevel:Default", "Warning"), + + }).Build(); + + IServiceCollection collection = new ServiceCollection() + .AddSingleton(config) + .AddLogging(builder => + { + builder.AddConfiguration(config); + builder.AddAppInsightsLogger(); + }); + + var logger = collection.BuildServiceProvider().GetService() as AppInsightsLogger; + + logger.Should().NotBeNull(); + logger.LogLevel.Should().Be(LogLevel.Warning); + logger.InstrumentationKey.Should().Be("test"); + } + + [Fact, IsUnit] + public void Test_BuilderExtension_Error() + { + IConfiguration config = new ConfigurationBuilder().AddInMemoryCollection(new List>() + { + new KeyValuePair("Logging:Instrumentationkey", "test"), + new KeyValuePair("Logging:LogLevel:Default", "Error"), + + }).Build(); + + IServiceCollection collection = new ServiceCollection() + .AddSingleton(config) + .AddLogging(builder => + { + builder.AddConfiguration(config); + builder.AddAppInsightsLogger(); + }); + + var logger = collection.BuildServiceProvider().GetService() as AppInsightsLogger; + + logger.Should().NotBeNull(); + logger.LogLevel.Should().Be(LogLevel.Error); + logger.InstrumentationKey.Should().Be("test"); + } + + [Fact, IsUnit] + public void Test_BuilderExtension_None() + { + IConfiguration config = new ConfigurationBuilder().AddInMemoryCollection(new List>() + { + new KeyValuePair("Logging:Instrumentationkey", "test"), + new KeyValuePair("Logging:LogLevel:Default", "None"), + + }).Build(); + + IServiceCollection collection = new ServiceCollection() + .AddSingleton(config) + .AddLogging(builder => + { + builder.AddConfiguration(config); + builder.AddAppInsightsLogger(); + }); + + var logger = collection.BuildServiceProvider().GetService() as AppInsightsLogger; + + logger.Should().NotBeNull(); + logger.LogLevel.Should().Be(LogLevel.None); + logger.InstrumentationKey.Should().Be("test"); + } + + [Fact, IsUnit] + public void Test_BuilderExtension_Trace() + { + IConfiguration config = new ConfigurationBuilder().AddInMemoryCollection(new List>() + { + new KeyValuePair("Logging:Instrumentationkey", "test"), + new KeyValuePair("Logging:LogLevel:Default", "Trace"), + + }).Build(); + + IServiceCollection collection = new ServiceCollection() + .AddSingleton(config) + .AddLogging(builder => + { + builder.AddConfiguration(config); + builder.AddAppInsightsLogger(); + }); + + var logger = collection.BuildServiceProvider().GetService() as AppInsightsLogger; + + logger.Should().NotBeNull(); + logger.LogLevel.Should().Be(LogLevel.Trace); + logger.InstrumentationKey.Should().Be("test"); + } + } +} diff --git a/src/Tests/Cloud.Core.Telemetry.AzureAppInsights.Tests.csproj b/src/Tests/Cloud.Core.Telemetry.AzureAppInsights.Tests.csproj new file mode 100644 index 0000000..27b41b4 --- /dev/null +++ b/src/Tests/Cloud.Core.Telemetry.AzureAppInsights.Tests.csproj @@ -0,0 +1,33 @@ + + + + netcoreapp2.1 + false + Robert McCabe + Robert McCabe + Unit tests for Cloud.Core.Telemetry.AzureAppInsights package. + Robert McCabe + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + +