diff --git a/src/Logging.XUnit/XUnitLogScope.cs b/src/Logging.XUnit/XUnitLogScope.cs index 236f83fe..367161e0 100644 --- a/src/Logging.XUnit/XUnitLogScope.cs +++ b/src/Logging.XUnit/XUnitLogScope.cs @@ -16,20 +16,20 @@ internal sealed class XUnitLogScope /// private static readonly AsyncLocal _value = new AsyncLocal(); - /// - /// The state object for the scope. - /// - private readonly object _state; - /// /// Initializes a new instance of the class. /// /// The state object for the scope. internal XUnitLogScope(object state) { - _state = state; + State = state; } + /// + /// Gets the state object for the scope. + /// + public object State { get; } + /// /// Gets or sets the current scope. /// @@ -46,7 +46,7 @@ internal static XUnitLogScope Current /// public override string ToString() - => _state.ToString(); + => State.ToString(); /// /// Pushes a new value into the scope. diff --git a/src/Logging.XUnit/XUnitLogger.cs b/src/Logging.XUnit/XUnitLogger.cs index 1e6a9940..e66089d5 100644 --- a/src/Logging.XUnit/XUnitLogger.cs +++ b/src/Logging.XUnit/XUnitLogger.cs @@ -2,6 +2,8 @@ // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. using System; +using System.Collections; +using System.Collections.Generic; using System.Text; using Microsoft.Extensions.Logging; using Xunit.Abstractions; @@ -269,16 +271,20 @@ private static void GetScopeInformation(StringBuilder builder) while (current != null) { - if (length == builder.Length) + foreach (var property in StringifyScope(current)) { - scopeLog = $"=> {current}"; - } - else - { - scopeLog = $"=> {current} "; + if (length == builder.Length) + { + scopeLog = $"=> {property}"; + } + else + { + scopeLog = $"=> {property} "; + } + + builder.Insert(length, scopeLog); } - builder.Insert(length, scopeLog); current = current.Parent; } @@ -288,5 +294,32 @@ private static void GetScopeInformation(StringBuilder builder) builder.AppendLine(); } } + + /// + /// Returns one or more stringified properties from the log scope. + /// + /// The to stringify. + /// An enumeration of scope properties from the current scope. + private static IEnumerable StringifyScope(XUnitLogScope scope) + { + if (scope.State is IEnumerable> pairs) + { + foreach (var pair in pairs) + { + yield return $"{pair.Key}: {pair.Value}"; + } + } + else if (scope.State is IEnumerable entries) + { + foreach (var entry in entries) + { + yield return entry; + } + } + else + { + yield return scope.ToString(); + } + } } } diff --git a/tests/Logging.XUnit.Tests/XUnitLoggerTests.cs b/tests/Logging.XUnit.Tests/XUnitLoggerTests.cs index 708f3153..958b6c38 100644 --- a/tests/Logging.XUnit.Tests/XUnitLoggerTests.cs +++ b/tests/Logging.XUnit.Tests/XUnitLoggerTests.cs @@ -2,6 +2,7 @@ // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. using System; +using System.Collections.Generic; using Microsoft.Extensions.Logging; using Moq; using Shouldly; @@ -473,6 +474,82 @@ public static void XUnitLogger_Log_Logs_Message_If_Scopes_Included_And_There_Are mock.Verify((p) => p.WriteLine(expected), Times.Once()); } + [Fact] + public static void XUnitLogger_Log_Logs_Message_If_Scopes_Included_And_There_Is_Scope_Of_KeyValuePair() + { + // Arrange + var mock = new Mock(); + + string name = "MyName"; + var outputHelper = mock.Object; + + var options = new XUnitLoggerOptions() + { + Filter = FilterTrue, + IncludeScopes = true, + }; + + var logger = new XUnitLogger(name, outputHelper, options) + { + Clock = StaticClock, + }; + + string expected = string.Join( + Environment.NewLine, + new[] { "[2018-08-19 16:12:16Z] info: MyName[0]", " => ScopeKey: ScopeValue", " Message|False|False" }); + + // Act + using (logger.BeginScope(new[] + { + new KeyValuePair("ScopeKey", "ScopeValue"), + })) + { + logger.Log(LogLevel.Information, 0, null, null, Formatter); + } + + // Assert + mock.Verify((p) => p.WriteLine(expected), Times.Once()); + } + + [Fact] + public static void XUnitLogger_Log_Logs_Message_If_Scopes_Included_And_There_Is_Scope_Of_KeyValuePairs() + { + // Arrange + var mock = new Mock(); + + string name = "MyName"; + var outputHelper = mock.Object; + + var options = new XUnitLoggerOptions() + { + Filter = FilterTrue, + IncludeScopes = true, + }; + + var logger = new XUnitLogger(name, outputHelper, options) + { + Clock = StaticClock, + }; + + string expected = string.Join( + Environment.NewLine, + new[] { "[2018-08-19 16:12:16Z] info: MyName[0]", " => ScopeKeyThree: ScopeValueThree => ScopeKeyTwo: ScopeValueTwo => ScopeKeyOne: ScopeValueOne", " Message|False|False" }); + + // Act + using (logger.BeginScope(new[] + { + new KeyValuePair("ScopeKeyOne", "ScopeValueOne"), + new KeyValuePair("ScopeKeyTwo", "ScopeValueTwo"), + new KeyValuePair("ScopeKeyThree", "ScopeValueThree"), + })) + { + logger.Log(LogLevel.Information, 0, null, null, Formatter); + } + + // Assert + mock.Verify((p) => p.WriteLine(expected), Times.Once()); + } + private static DateTimeOffset StaticClock() => new DateTimeOffset(2018, 08, 19, 17, 12, 16, TimeSpan.FromHours(1)); private static bool FilterTrue(string categoryName, LogLevel level) => true;