Skip to content

Commit

Permalink
Merge pull request #215 from gkinsman/detect-keyvaluepairs
Browse files Browse the repository at this point in the history
Detect IEnumerable<KeyValuePair<string, object>> in log scopes
  • Loading branch information
martincostello authored Mar 26, 2021
2 parents 6a2f6c5 + 2d38c4e commit 4ca0b65
Show file tree
Hide file tree
Showing 3 changed files with 233 additions and 21 deletions.
14 changes: 7 additions & 7 deletions src/Logging.XUnit/XUnitLogScope.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,20 @@ internal sealed class XUnitLogScope
/// </summary>
private static readonly AsyncLocal<XUnitLogScope> _value = new AsyncLocal<XUnitLogScope>();

/// <summary>
/// The state object for the scope.
/// </summary>
private readonly object _state;

/// <summary>
/// Initializes a new instance of the <see cref="XUnitLogScope"/> class.
/// </summary>
/// <param name="state">The state object for the scope.</param>
internal XUnitLogScope(object state)
{
_state = state;
State = state;
}

/// <summary>
/// Gets the state object for the scope.
/// </summary>
public object State { get; }

/// <summary>
/// Gets or sets the current scope.
/// </summary>
Expand All @@ -46,7 +46,7 @@ internal static XUnitLogScope Current

/// <inheritdoc />
public override string ToString()
=> _state.ToString();
=> State.ToString();

/// <summary>
/// Pushes a new value into the scope.
Expand Down
58 changes: 45 additions & 13 deletions src/Logging.XUnit/XUnitLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
// 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.Linq;
using System.Text;
using Microsoft.Extensions.Logging;
using Xunit.Abstractions;
Expand Down Expand Up @@ -264,28 +267,57 @@ private static string GetLogLevelString(LogLevel logLevel)
private static void GetScopeInformation(StringBuilder builder)
{
var current = XUnitLogScope.Current;
string scopeLog;
int length = builder.Length;

var stack = new Stack<XUnitLogScope>();
while (current != null)
{
if (length == builder.Length)
{
scopeLog = $"=> {current}";
}
else
stack.Push(current);
current = current.Parent;
}

var depth = 0;
static string DepthPadding(int depth) => new string(' ', depth * 2);

while (stack.Count > 0)
{
var elem = stack.Pop();
foreach (var property in StringifyScope(elem))
{
scopeLog = $"=> {current} ";
builder.Append(MessagePadding)
.Append(DepthPadding(depth))
.Append("=> ")
.Append(property)
.AppendLine();
}

builder.Insert(length, scopeLog);
current = current.Parent;
depth++;
}
}

if (builder.Length > length)
/// <summary>
/// Returns one or more stringified properties from the log scope.
/// </summary>
/// <param name="scope">The <see cref="XUnitLogScope"/> to stringify.</param>
/// <returns>An enumeration of scope properties from the current scope.</returns>
private static IEnumerable<string> StringifyScope(XUnitLogScope scope)
{
if (scope.State is IEnumerable<KeyValuePair<string, object>> pairs)
{
foreach (var pair in pairs)
{
yield return pair.Key + ": " + pair.Value;
}
}
else if (scope.State is IEnumerable<string> entries)
{
foreach (var entry in entries)
{
yield return entry;
}
}
else
{
builder.Insert(length, MessagePadding);
builder.AppendLine();
yield return scope.ToString();
}
}
}
Expand Down
182 changes: 181 additions & 1 deletion tests/Logging.XUnit.Tests/XUnitLoggerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -452,7 +453,12 @@ public static void XUnitLogger_Log_Logs_Message_If_Scopes_Included_And_There_Are

string expected = string.Join(
Environment.NewLine,
new[] { "[2018-08-19 16:12:16Z] info: MyName[0]", " => _ => __ => ___ => [null]", " Message|False|False" });
"[2018-08-19 16:12:16Z] info: MyName[0]",
" => _",
" => __",
" => ___",
" => {OriginalFormat}: [null]",
" Message|False|False");

// Act
using (logger.BeginScope("_"))
Expand All @@ -473,6 +479,180 @@ 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<ITestOutputHelper>();

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,
"[2018-08-19 16:12:16Z] info: MyName[0]",
" => ScopeKey: ScopeValue",
" Message|False|False");

// Act
using (logger.BeginScope(new[]
{
new KeyValuePair<string, object>("ScopeKey", "ScopeValue"),
}))
{
logger.Log<string>(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<ITestOutputHelper>();

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,
"[2018-08-19 16:12:16Z] info: MyName[0]",
" => ScopeKeyOne: ScopeValueOne",
" => ScopeKeyTwo: ScopeValueTwo",
" => ScopeKeyThree: ScopeValueThree",
" Message|False|False");

// Act
using (logger.BeginScope(new[]
{
new KeyValuePair<string, object>("ScopeKeyOne", "ScopeValueOne"),
new KeyValuePair<string, object>("ScopeKeyTwo", "ScopeValueTwo"),
new KeyValuePair<string, object>("ScopeKeyThree", "ScopeValueThree"),
}))
{
logger.Log<string>(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_Are_Scopes_Of_KeyValuePairs()
{
// Arrange
var mock = new Mock<ITestOutputHelper>();

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,
"[2018-08-19 16:12:16Z] info: MyName[0]",
" => ScopeKeyOne: ScopeValueOne",
" => ScopeKeyTwo: ScopeValueTwo",
" => ScopeKeyThree: ScopeValueThree",
" => ScopeKeyFour: ScopeValueFour",
" => ScopeKeyFive: ScopeValueFive",
" => ScopeKeySix: ScopeValueSix",
" Message|False|False");

// Act
using (logger.BeginScope(new[]
{
new KeyValuePair<string, object>("ScopeKeyOne", "ScopeValueOne"),
new KeyValuePair<string, object>("ScopeKeyTwo", "ScopeValueTwo"),
new KeyValuePair<string, object>("ScopeKeyThree", "ScopeValueThree"),
}))
{
using (logger.BeginScope(new[]
{
new KeyValuePair<string, object>("ScopeKeyFour", "ScopeValueFour"),
new KeyValuePair<string, object>("ScopeKeyFive", "ScopeValueFive"),
new KeyValuePair<string, object>("ScopeKeySix", "ScopeValueSix"),
}))
{
logger.Log<string>(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_IEnumerable()
{
// Arrange
var mock = new Mock<ITestOutputHelper>();

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,
"[2018-08-19 16:12:16Z] info: MyName[0]",
" => ScopeKeyOne",
" => ScopeKeyTwo",
" => ScopeKeyThree",
" Message|False|False");

// Act
using (logger.BeginScope(new[] { "ScopeKeyOne", "ScopeKeyTwo", "ScopeKeyThree" }))
{
logger.Log<string>(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;
Expand Down

0 comments on commit 4ca0b65

Please sign in to comment.