Skip to content

Commit 2f1b598

Browse files
committed
Initial implementation of Microsoft.Extensions.Logging
1 parent a6a9adb commit 2f1b598

File tree

6 files changed

+336
-1
lines changed

6 files changed

+336
-1
lines changed

Exceptionless.Net.sln

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
33
# Visual Studio 15
4-
VisualStudioVersion = 15.0.26228.9
4+
VisualStudioVersion = 15.0.26730.12
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{728C18BC-4085-4492-B0B2-8211CA209A50}"
77
ProjectSection(SolutionItems) = preProject
@@ -88,6 +88,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Exceptionless.SampleAspNetC
8888
EndProject
8989
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Exceptionless.SampleConsole", "samples\Exceptionless.SampleConsole\Exceptionless.SampleConsole.csproj", "{23645F73-57D8-4D70-BE1A-37903A8E4A18}"
9090
EndProject
91+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Exceptionless.Extensions.Logging", "src\Exceptionless.Extensions.Logging\Exceptionless.Extensions.Logging.csproj", "{CA402AB4-D9CE-4053-AE26-94C1B3A4E48E}"
92+
EndProject
9193
Global
9294
GlobalSection(SolutionConfigurationPlatforms) = preSolution
9395
Debug|Any CPU = Debug|Any CPU
@@ -174,6 +176,10 @@ Global
174176
{23645F73-57D8-4D70-BE1A-37903A8E4A18}.Debug|Any CPU.Build.0 = Debug|Any CPU
175177
{23645F73-57D8-4D70-BE1A-37903A8E4A18}.Release|Any CPU.ActiveCfg = Release|Any CPU
176178
{23645F73-57D8-4D70-BE1A-37903A8E4A18}.Release|Any CPU.Build.0 = Release|Any CPU
179+
{CA402AB4-D9CE-4053-AE26-94C1B3A4E48E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
180+
{CA402AB4-D9CE-4053-AE26-94C1B3A4E48E}.Debug|Any CPU.Build.0 = Debug|Any CPU
181+
{CA402AB4-D9CE-4053-AE26-94C1B3A4E48E}.Release|Any CPU.ActiveCfg = Release|Any CPU
182+
{CA402AB4-D9CE-4053-AE26-94C1B3A4E48E}.Release|Any CPU.Build.0 = Release|Any CPU
177183
EndGlobalSection
178184
GlobalSection(SolutionProperties) = preSolution
179185
HideSolutionNode = FALSE
@@ -198,4 +204,7 @@ Global
198204
{D7039656-8587-49CC-8657-5C9BDCFC1C80} = {2CEE12C6-3840-4C01-A952-D3026B0A662A}
199205
{23645F73-57D8-4D70-BE1A-37903A8E4A18} = {2CEE12C6-3840-4C01-A952-D3026B0A662A}
200206
EndGlobalSection
207+
GlobalSection(ExtensibilityGlobals) = postSolution
208+
SolutionGuid = {EBB2CC85-FF87-431B-865F-2F110B2A10E6}
209+
EndGlobalSection
201210
EndGlobal
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFrameworks>netstandard1.3;netstandard2.0;net46;net461</TargetFrameworks>
5+
<RootNamespace>Exceptionless.Extensions.Logging</RootNamespace>
6+
<Version>4.0.0</Version>
7+
<Copyright>Copyright (c) 2017 Exceptionless. All rights reserved.</Copyright>
8+
<PackageLicenseUrl>http://www.apache.org/licenses/LICENSE-2.0</PackageLicenseUrl>
9+
<PackageProjectUrl>https://github.com/exceptionless/Exceptionless.Net</PackageProjectUrl>
10+
<PackageIconUrl>https://be.exceptionless.io/img/exceptionless-32.png</PackageIconUrl>
11+
<RepositoryUrl>https://github.com/exceptionless/Exceptionless.Net</RepositoryUrl>
12+
<RepositoryType>git</RepositoryType>
13+
<PackageReleaseNotes>https://github.com/exceptionless/Exceptionless.Net/releases</PackageReleaseNotes>
14+
<PackageTags>Exceptionless;Microsoft.Extensions.Logging;log;logging</PackageTags>
15+
<Description>Exceptionless provider for Microsoft.Extensions.Logging.</Description>
16+
</PropertyGroup>
17+
18+
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard1.3'">
19+
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="1.1.2" />
20+
</ItemGroup>
21+
22+
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
23+
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.0.0" />
24+
</ItemGroup>
25+
26+
<ItemGroup Condition="'$(TargetFramework)' == 'net46'">
27+
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="1.1.2" />
28+
</ItemGroup>
29+
30+
<ItemGroup Condition="'$(TargetFramework)' == 'net461'">
31+
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.0.0" />
32+
</ItemGroup>
33+
34+
<ItemGroup>
35+
<ProjectReference Include="..\Exceptionless\Exceptionless.csproj" />
36+
</ItemGroup>
37+
38+
</Project>
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
using Exceptionless.Models;
2+
using Microsoft.Extensions.Logging;
3+
using System;
4+
using System.Collections.Generic;
5+
6+
namespace Exceptionless.Extensions.Logging
7+
{
8+
public class ExceptionlessLogger : ILogger
9+
{
10+
static readonly Dictionary<LogLevel, string> LOG_LEVELS = MapLogLevelsToExceptionlessNames();
11+
12+
static Dictionary<LogLevel, string> MapLogLevelsToExceptionlessNames()
13+
{
14+
Dictionary<LogLevel, string> mappings = new Dictionary<LogLevel, string>(7);
15+
mappings.Add(LogLevel.Critical, global::Exceptionless.Logging.LogLevel.Fatal.ToString());
16+
mappings.Add(LogLevel.Debug, global::Exceptionless.Logging.LogLevel.Debug.ToString());
17+
mappings.Add(LogLevel.Error, global::Exceptionless.Logging.LogLevel.Error.ToString());
18+
mappings.Add(LogLevel.Information, global::Exceptionless.Logging.LogLevel.Info.ToString());
19+
mappings.Add(LogLevel.None, global::Exceptionless.Logging.LogLevel.Off.ToString());
20+
mappings.Add(LogLevel.Trace, global::Exceptionless.Logging.LogLevel.Trace.ToString());
21+
mappings.Add(LogLevel.Warning, global::Exceptionless.Logging.LogLevel.Warn.ToString());
22+
23+
return mappings;
24+
}
25+
26+
ExceptionlessClient _Client;
27+
string _Source;
28+
29+
/// <summary>
30+
/// Initializes a new instance of the <see cref="ExceptionlessLogger"/> class.
31+
/// </summary>
32+
/// <param name="client">The <see cref="ExceptionlessClient"/> to be used by the logger.</param>
33+
/// <param name="source">The source to tag events with, typically the category.</param>
34+
public ExceptionlessLogger(ExceptionlessClient client, string source)
35+
{
36+
if (client == null)
37+
throw new ArgumentNullException(nameof(client));
38+
39+
_Client = client;
40+
_Source = source;
41+
}
42+
43+
/// <summary>
44+
/// Begins a logical operation scope.
45+
/// </summary>The identifier for the scope.
46+
/// <typeparam name="TState">The type of the state object.</typeparam>
47+
/// <param name="state"></param>
48+
/// <returns>An <see cref="IDisposable"/> that ends the logical operation scope on dispose.</returns>
49+
public IDisposable BeginScope<TState>(TState state)
50+
{
51+
if (state == null)
52+
throw new ArgumentNullException(nameof(state));
53+
54+
// Typically a string will be used as the state for a scope, but the BeginScope extension provides
55+
// a FormattedLogValues and ASP.NET provides multiple scope objects, so just use ToString()
56+
string description = state.ToString();
57+
58+
// If there's data other than a simple string, it'll be added to the event
59+
object stateObj = state is string ? null : (object)state;
60+
61+
// Log scope creation as an event so that there is a parent to tie events together
62+
ExceptionlessLoggingScope scope = new ExceptionlessLoggingScope(description);
63+
LogScope(scope, stateObj);
64+
65+
// Add to stack to support nesting within execution context
66+
ExceptionlessLoggingScope.Push(scope);
67+
return scope;
68+
}
69+
70+
/// <summary>
71+
/// Checks if the given <see cref="LogLevel"/> is enabled.
72+
/// </summary>
73+
/// <param name="logLevel">The level to be checked.</param>
74+
/// <returns>Returns true if enabled.</returns>
75+
public bool IsEnabled(LogLevel logLevel)
76+
{
77+
return true;
78+
}
79+
80+
/// <summary>
81+
/// Writes a log entry.
82+
/// </summary>
83+
/// <typeparam name="TState">The type of the state object.</typeparam>
84+
/// <param name="logLevel">Entry will be written on this level.</param>
85+
/// <param name="eventId">Id of the event.</param>
86+
/// <param name="state">The entry to be written. Can be also an object.</param>
87+
/// <param name="exception">The exception related to this entry.</param>
88+
/// <param name="formatter">Function to create a <see cref="string"/> message of the <paramref name="state"/> and <paramref name="exception"/>.</param>
89+
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
90+
{
91+
// Create basic event from client
92+
EventBuilder eb = exception == null ? _Client.CreateEvent() : _Client.CreateException(exception);
93+
eb.SetProperty(Event.KnownDataKeys.Level, LOG_LEVELS[logLevel]);
94+
95+
// Get formatted message
96+
eb.SetMessage(formatter.Invoke(state, exception));
97+
98+
// Add category as source, if available
99+
if (_Source != null)
100+
eb.SetSource(_Source);
101+
102+
// Add event id, if available
103+
if (eventId.Id != 0)
104+
eb.SetProperty("Event Id", eventId.Id);
105+
106+
// If within a scope, add scope's reference id
107+
if (ExceptionlessLoggingScope.Current != null)
108+
eb.SetEventReference("Parent", ExceptionlessLoggingScope.Current.Id);
109+
110+
// The logging framework passes in FormattedLogValues, which implements IEnumerable<KeyValuePair<string, object>>;
111+
// add each property and value as individual objects for proper visibility in Exceptionless
112+
IEnumerable<KeyValuePair<string, object>> stateProps = state as IEnumerable<KeyValuePair<string, object>>;
113+
if (stateProps != null)
114+
{
115+
foreach (KeyValuePair<string, object> prop in stateProps)
116+
{
117+
// Logging the message template is superfluous
118+
if (prop.Key != "{OriginalFormat}")
119+
eb.AddObject(prop.Value, prop.Key);
120+
}
121+
}
122+
// Otherwise, attach the entire object, using its type as the name
123+
else
124+
{
125+
eb.AddObject(state);
126+
}
127+
128+
// Add to client's queue
129+
eb.Submit();
130+
}
131+
132+
/// <summary>
133+
/// Writes a scope creation entry.
134+
/// </summary>
135+
/// <param name="newScope">The <see cref="ExceptionlessLoggingScope"/> being created.</param>
136+
private void LogScope(ExceptionlessLoggingScope newScope, object state)
137+
{
138+
EventBuilder eb = _Client.CreateLog($"Creating scope: {newScope.Description}.", global::Exceptionless.Logging.LogLevel.Other);
139+
140+
// Set event reference id to that of scope object
141+
eb.SetReferenceId(newScope.Id);
142+
143+
// If this is a nested scope, add parent's reference id
144+
if (ExceptionlessLoggingScope.Current != null)
145+
eb.SetEventReference("Parent", ExceptionlessLoggingScope.Current.Id);
146+
147+
if (state != null)
148+
{
149+
IEnumerable<KeyValuePair<string, object>> stateProps = state as IEnumerable<KeyValuePair<string, object>>;
150+
if (stateProps != null)
151+
{
152+
foreach (KeyValuePair<string, object> prop in stateProps)
153+
{
154+
// Logging the message template is superfluous
155+
if (prop.Key != "{OriginalFormat}")
156+
eb.AddObject(prop.Value, prop.Key);
157+
}
158+
}
159+
else
160+
{
161+
eb.AddObject(state);
162+
}
163+
}
164+
165+
eb.Submit();
166+
}
167+
}
168+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using Microsoft.Extensions.Logging;
2+
using System;
3+
4+
namespace Exceptionless.Extensions.Logging
5+
{
6+
public static class ExceptionlessLoggerExtensions
7+
{
8+
/// <summary>
9+
/// Adds Exceptionless to the logging pipeline.
10+
/// </summary>
11+
/// <param name="factory">The <see cref="ILoggerFactory"/>.</param>
12+
/// <param name="apiKey">The project api key.</param>
13+
/// <returns>The <see cref="ILoggerFactory"/>.</returns>
14+
public static ILoggerFactory AddExceptionless(this ILoggerFactory factory, string apiKey)
15+
{
16+
ExceptionlessConfiguration config = new ExceptionlessConfiguration(ExceptionlessClient.Default.Configuration.Resolver);
17+
config.ApiKey = apiKey;
18+
19+
factory.AddProvider(new ExceptionlessLoggerProvider(config));
20+
return factory;
21+
}
22+
23+
/// <summary>
24+
/// Adds Exceptionless to the logging pipeline.
25+
/// </summary>
26+
/// <param name="factory">The <see cref="ILoggerFactory"/>.</param>
27+
/// <param name="config">An <see cref="ExceptionlessConfiguration"/> containing additional settings and plugins. The project api key must be specified.</param>
28+
/// <returns>The <see cref="ILoggerFactory"/>.</returns>
29+
public static ILoggerFactory AddExceptionless(this ILoggerFactory factory, ExceptionlessConfiguration config)
30+
{
31+
factory.AddProvider(new ExceptionlessLoggerProvider(config));
32+
return factory;
33+
}
34+
}
35+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using Microsoft.Extensions.Logging;
2+
using System;
3+
4+
namespace Exceptionless.Extensions.Logging
5+
{
6+
public class ExceptionlessLoggerProvider : ILoggerProvider
7+
{
8+
ExceptionlessClient _Client;
9+
10+
/// <summary>
11+
/// Initializes a new instance of the <see cref="ExceptionlessLoggerProvider"/> class.
12+
/// </summary>
13+
/// <param name="config">An <see cref="ExceptionlessConfiguration"/> which will be provided to created loggers.</param>
14+
public ExceptionlessLoggerProvider(ExceptionlessConfiguration config)
15+
{
16+
if (config == null)
17+
throw new ArgumentNullException(nameof(config));
18+
19+
_Client = new ExceptionlessClient(config);
20+
_Client.Startup();
21+
}
22+
23+
/// <summary>
24+
/// Creates a new <see cref="ILogger"/> instance.
25+
/// </summary>
26+
/// <param name="categoryName">The category name for messages produced by the logger.</param>
27+
/// <returns>An <see cref="ILogger"/></returns>
28+
public ILogger CreateLogger(string categoryName)
29+
{
30+
return new ExceptionlessLogger(_Client, categoryName);
31+
}
32+
33+
public void Dispose()
34+
{
35+
_Client.Shutdown();
36+
}
37+
}
38+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using System;
2+
using System.Threading;
3+
4+
namespace Exceptionless.Extensions.Logging
5+
{
6+
internal class ExceptionlessLoggingScope : IDisposable
7+
{
8+
private static AsyncLocal<ExceptionlessLoggingScope> _Current = new AsyncLocal<ExceptionlessLoggingScope>();
9+
public static ExceptionlessLoggingScope Current
10+
{
11+
get
12+
{
13+
return _Current.Value;
14+
}
15+
16+
set
17+
{
18+
_Current.Value = value;
19+
}
20+
21+
}
22+
23+
public static void Push(ExceptionlessLoggingScope scope)
24+
{
25+
var temp = Current;
26+
Current = scope;
27+
Current.Parent = temp;
28+
}
29+
30+
public string Description { get; private set; }
31+
32+
public string Id { get; private set; }
33+
34+
public ExceptionlessLoggingScope Parent { get; private set; }
35+
36+
public ExceptionlessLoggingScope(string description)
37+
{
38+
Description = description;
39+
Id = Guid.NewGuid().ToString();
40+
}
41+
42+
public void Dispose()
43+
{
44+
Current = Current.Parent;
45+
}
46+
}
47+
}

0 commit comments

Comments
 (0)