Skip to content

Commit 6b78d50

Browse files
authored
Merge pull request #170 from dpvreony/logging
feature: migrate logging helper from whipstaff + foundatio
2 parents 6899607 + 7fd8845 commit 6b78d50

19 files changed

+764
-192
lines changed

src/Directory.build.targets

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
<Project>
2+
<PropertyGroup Condition="$(TargetFramework.StartsWith('netstandard'))">
3+
<DefineConstants>$(DefineConstants);NETSTANDARD;PORTABLE;ARGUMENT_NULL_EXCEPTION_SHIM</DefineConstants>
4+
</PropertyGroup>
5+
<PropertyGroup Condition="$(TargetFramework.StartsWith('netstandard2'))">
6+
<DefineConstants>$(DefineConstants);CALLER_ARGUMENT_EXPRESSION_SHIM;IS_EXTERNAL_INIT_SHIM</DefineConstants>
7+
</PropertyGroup>
28
<PropertyGroup Condition="$(TargetFramework.StartsWith('net4'))">
39
<DefineConstants>$(DefineConstants);NET_45;XAML</DefineConstants>
410
</PropertyGroup>
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// Copyright (c) 2019 dpvreony and Contributors. All rights reserved.
2+
// This file is licensed to you under the MIT license.
3+
// See the LICENSE file in the project root for full license information.
4+
5+
using System;
6+
using System.Collections.Generic;
7+
using Microsoft.Extensions.Logging;
8+
9+
namespace NetTestRegimentation.XUnit.Logging
10+
{
11+
/// <summary>
12+
/// Represents a log entry.
13+
/// </summary>
14+
public sealed class LogEntry
15+
{
16+
/// <summary>
17+
/// Initializes a new instance of the <see cref="LogEntry"/> class.
18+
/// </summary>
19+
/// <param name="date">The date and time of the log entry.</param>
20+
/// <param name="categoryName">The category name of the log entry.</param>
21+
/// <param name="logLevel">The log level of the log entry.</param>
22+
/// <param name="state">The state of the log entry.</param>
23+
/// <param name="formatter">The formatter of the log entry.</param>
24+
public LogEntry(DateTimeOffset date, string categoryName, LogLevel logLevel, object state, Func<object, Exception?, string> formatter)
25+
{
26+
Date = date;
27+
CategoryName = categoryName;
28+
LogLevel = logLevel;
29+
State = state;
30+
Formatter = formatter;
31+
}
32+
33+
/// <summary>
34+
/// Gets or sets the date and time of the log entry.
35+
/// </summary>
36+
public DateTimeOffset Date { get; set; }
37+
38+
/// <summary>
39+
/// Gets or sets the category name of the log entry.
40+
/// </summary>
41+
public string CategoryName { get; set; }
42+
43+
/// <summary>
44+
/// Gets or sets the log level of the log entry.
45+
/// </summary>
46+
public LogLevel LogLevel { get; set; }
47+
48+
/// <summary>
49+
/// Gets or sets the scopes of the log entry.
50+
/// </summary>
51+
#pragma warning disable CA1819 // Properties should not return arrays
52+
public object[]? Scopes { get; set; }
53+
#pragma warning restore CA1819 // Properties should not return arrays
54+
55+
/// <summary>
56+
/// Gets or sets the event id of the log entry.
57+
/// </summary>
58+
public EventId? EventId { get; set; }
59+
60+
/// <summary>
61+
/// Gets or sets the state of the log entry.
62+
/// </summary>
63+
public object State { get; set; }
64+
65+
/// <summary>
66+
/// Gets or sets the exception of the log entry.
67+
/// </summary>
68+
public Exception? Exception { get; set; }
69+
70+
/// <summary>
71+
/// Gets or sets the formatter of the log entry.
72+
/// </summary>
73+
public Func<object, Exception?, string> Formatter { get; set; }
74+
75+
/// <summary>
76+
/// Gets the properties of the log entry.
77+
/// </summary>
78+
public IDictionary<string, object> Properties { get; } = new Dictionary<string, object>();
79+
80+
/// <summary>
81+
/// Gets the message of the log entry.
82+
/// </summary>
83+
public string Message => Formatter(State, Exception);
84+
85+
/// <summary>Returns a string that represents the current object.</summary>
86+
/// <returns>A string that represents the current object.</returns>
87+
public override string ToString()
88+
{
89+
#pragma warning disable CA1305 // Specify IFormatProvider
90+
#pragma warning disable CA1304 // Specify CultureInfo
91+
#pragma warning disable CA1311 // Specify a culture or use an invariant version
92+
return string.Concat(string.Empty, Date.ToString("mm:ss.fff"), " ", LogLevel.ToString().Substring(0, 1).ToUpper(), ": ", Message, " - ", CategoryName);
93+
#pragma warning restore CA1311 // Specify a culture or use an invariant version
94+
#pragma warning restore CA1304 // Specify CultureInfo
95+
#pragma warning restore CA1305 // Specify IFormatProvider
96+
}
97+
98+
/// <summary>
99+
/// Returns a string that represents the current object.
100+
/// </summary>
101+
/// <param name="useFullCategory">
102+
/// Whether to use the full category name or just the last part.
103+
/// </param>
104+
/// <returns>A string that represents the current object.</returns>
105+
public string ToString(bool useFullCategory)
106+
{
107+
string category = CategoryName;
108+
if (!useFullCategory)
109+
{
110+
int lastDot = category.LastIndexOf('.');
111+
if (lastDot >= 0)
112+
{
113+
category = category.Substring(lastDot + 1);
114+
}
115+
}
116+
117+
#pragma warning disable CA1305 // Specify IFormatProvider
118+
#pragma warning disable CA1304 // Specify CultureInfo
119+
#pragma warning disable CA1311 // Specify a culture or use an invariant version
120+
return string.Concat(string.Empty, Date.ToString("mm:ss.fff"), " ", LogLevel.ToString().Substring(0, 1).ToUpper(), ": ", Message, " - ", category);
121+
#pragma warning restore CA1311 // Specify a culture or use an invariant version
122+
#pragma warning restore CA1304 // Specify CultureInfo
123+
#pragma warning restore CA1305 // Specify IFormatProvider
124+
}
125+
}
126+
}
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
// Copyright (c) 2019 dpvreony and Contributors. All rights reserved.
2+
// This file is licensed to you under the MIT license.
3+
// See the LICENSE file in the project root for full license information.
4+
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Threading;
8+
using Microsoft.Extensions.Logging;
9+
using Xunit;
10+
11+
namespace NetTestRegimentation.XUnit.Logging
12+
{
13+
/// <summary>
14+
/// XUnit Test Logger Factory.
15+
/// </summary>
16+
public sealed class TestLogger : ILoggerFactory
17+
{
18+
private readonly Dictionary<string, LogLevel> _logLevels = new();
19+
private readonly Queue<LogEntry> _logEntries = new();
20+
private int _logEntriesWritten;
21+
22+
/// <summary>
23+
/// Initializes a new instance of the <see cref="TestLogger"/> class.
24+
/// </summary>
25+
/// <param name="configure">Action to invoke when configuring logging options.</param>
26+
public TestLogger(Action<TestLoggerOptions>? configure = null)
27+
{
28+
Options = new TestLoggerOptions();
29+
configure?.Invoke(Options);
30+
}
31+
32+
/// <summary>
33+
/// Initializes a new instance of the <see cref="TestLogger"/> class.
34+
/// </summary>
35+
/// <param name="output">XUnit output helper.</param>
36+
/// <param name="configure">Action to run when configuring logging options.</param>
37+
public TestLogger(ITestOutputHelper output, Action<TestLoggerOptions>? configure = null)
38+
{
39+
Options = new TestLoggerOptions
40+
{
41+
WriteLogEntryFunc = logEntry =>
42+
{
43+
output.WriteLine(logEntry.ToString(false));
44+
}
45+
};
46+
47+
configure?.Invoke(Options);
48+
}
49+
50+
/// <summary>
51+
/// Initializes a new instance of the <see cref="TestLogger"/> class.
52+
/// </summary>
53+
/// <param name="options">Settings for the logging configuration.</param>
54+
public TestLogger(TestLoggerOptions options)
55+
{
56+
Options = options ?? new TestLoggerOptions();
57+
}
58+
59+
/// <summary>
60+
/// Gets the options.
61+
/// </summary>
62+
public TestLoggerOptions Options { get; }
63+
64+
/// <summary>
65+
/// Gets or sets the default minimum log level.
66+
/// </summary>
67+
public LogLevel DefaultMinimumLevel
68+
{
69+
get => Options.DefaultMinimumLevel;
70+
set => Options.DefaultMinimumLevel = value;
71+
}
72+
73+
/// <summary>
74+
/// Gets or sets the maximum log entries to store.
75+
/// </summary>
76+
public int MaxLogEntriesToStore
77+
{
78+
get => Options.MaxLogEntriesToStore;
79+
set => Options.MaxLogEntriesToStore = value;
80+
}
81+
82+
/// <summary>
83+
/// Gets or sets the maximum log entries to write.
84+
/// </summary>
85+
public int MaxLogEntriesToWrite
86+
{
87+
get => Options.MaxLogEntriesToWrite;
88+
set => Options.MaxLogEntriesToWrite = value;
89+
}
90+
91+
/// <summary>
92+
/// Gets the log entries.
93+
/// </summary>
94+
public IReadOnlyList<LogEntry> LogEntries
95+
{
96+
get
97+
{
98+
lock (_logEntries)
99+
{
100+
return _logEntries.ToArray();
101+
}
102+
}
103+
}
104+
105+
/// <summary>
106+
/// Resets the log entries.
107+
/// </summary>
108+
public void Reset()
109+
{
110+
lock (_logEntries)
111+
{
112+
_logEntries.Clear();
113+
_logLevels.Clear();
114+
_ = Interlocked.Exchange(ref _logEntriesWritten, 0);
115+
}
116+
}
117+
118+
/// <inheritdoc/>
119+
public ILogger CreateLogger(string categoryName)
120+
{
121+
return new TestLoggerLogger(categoryName, this);
122+
}
123+
124+
/// <inheritdoc/>
125+
public void AddProvider(ILoggerProvider provider)
126+
{
127+
}
128+
129+
/// <summary>
130+
/// Checks if logging is enabled for the category at a specific log level.
131+
/// </summary>
132+
/// <param name="category">The category to check.</param>
133+
/// <param name="logLevel">The log level to check.</param>
134+
/// <returns>Whether the log level is enabled.</returns>
135+
public bool IsEnabled(string category, LogLevel logLevel)
136+
{
137+
if (_logLevels.TryGetValue(category, out var categoryLevel))
138+
{
139+
return logLevel >= categoryLevel;
140+
}
141+
142+
return logLevel >= Options.DefaultMinimumLevel;
143+
}
144+
145+
/// <summary>
146+
/// Sets the log level for a category.
147+
/// </summary>
148+
/// <param name="category">Category to set.</param>
149+
/// <param name="minLogLevel">LoggerFactory level to set.</param>
150+
public void SetLogLevel(string category, LogLevel minLogLevel)
151+
{
152+
_logLevels[category] = minLogLevel;
153+
}
154+
155+
/// <summary>
156+
/// Sets the log level for a category based on the type.
157+
/// </summary>
158+
/// <typeparam name="T">The type to base the category off.</typeparam>
159+
/// <param name="minLogLevel">LoggerFactory level to set.</param>
160+
public void SetLogLevel<T>(LogLevel minLogLevel)
161+
{
162+
SetLogLevel(nameof(T), minLogLevel);
163+
}
164+
165+
/// <inheritdoc/>
166+
public void Dispose()
167+
{
168+
}
169+
170+
internal void AddLogEntry(LogEntry logEntry)
171+
{
172+
lock (_logEntries)
173+
{
174+
_logEntries.Enqueue(logEntry);
175+
176+
if (_logEntries.Count > Options.MaxLogEntriesToStore)
177+
{
178+
_ = _logEntries.Dequeue();
179+
}
180+
}
181+
182+
if (Options.WriteLogEntryFunc == null || _logEntriesWritten >= Options.MaxLogEntriesToWrite)
183+
{
184+
return;
185+
}
186+
187+
#pragma warning disable CA1031 // Do not catch general exception types
188+
#pragma warning disable RCS1075 // Avoid empty catch clause that catches System.Exception
189+
try
190+
{
191+
Options.WriteLogEntry(logEntry);
192+
_ = Interlocked.Increment(ref _logEntriesWritten);
193+
}
194+
catch (Exception)
195+
{
196+
// ignored
197+
}
198+
#pragma warning restore RCS1075 // Avoid empty catch clause that catches System.Exception
199+
#pragma warning restore CA1031 // Do not catch general exception types
200+
}
201+
}
202+
}

0 commit comments

Comments
 (0)