Skip to content

Commit

Permalink
Log props: improve type support and filter flexibility (#102)
Browse files Browse the repository at this point in the history
  • Loading branch information
leftnet authored Jan 17, 2023
1 parent 096bb77 commit b3af038
Show file tree
Hide file tree
Showing 3 changed files with 190 additions and 38 deletions.
130 changes: 94 additions & 36 deletions src/Pact.Logging/LoggingExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@ public static void WithPactDefaults(this RequestLoggingOptions opts, string[] no
/// <param name="httpContext"></param>
public static void EnrichFromContext(IDiagnosticContext diagnosticContext, HttpContext httpContext)
{
diagnosticContext.Set("IdentityName", httpContext?.User?.Identity?.Name);
diagnosticContext.Set("RemoteIp", httpContext?.Connection?.RemoteIpAddress);
diagnosticContext.Set("IdentityName", httpContext?.User.Identity?.Name);
diagnosticContext.Set("RemoteIp", httpContext?.Connection.RemoteIpAddress);

if (httpContext?.Request?.Headers != null && httpContext.Request.Headers.ContainsKey(HeaderNames.UserAgent))
if (httpContext?.Request.Headers != null && httpContext.Request.Headers.ContainsKey(HeaderNames.UserAgent))
{
diagnosticContext.Set("Agent", httpContext.Request.Headers[HeaderNames.UserAgent]);
}
Expand All @@ -68,10 +68,10 @@ public static void EnrichFromContext(IDiagnosticContext diagnosticContext, HttpC
/// <param name="context"></param>
public static void EnrichFromFilterContext(IDiagnosticContext diagnosticContext, FilterContext context)
{
diagnosticContext.Set("RouteData", context?.ActionDescriptor?.RouteValues);
diagnosticContext.Set("ActionName", context?.ActionDescriptor?.DisplayName);
diagnosticContext.Set("ActionId", context?.ActionDescriptor?.Id);
diagnosticContext.Set("ValidationState", context?.ModelState?.IsValid);
diagnosticContext.Set("RouteData", context?.ActionDescriptor.RouteValues);
diagnosticContext.Set("ActionName", context?.ActionDescriptor.DisplayName);
diagnosticContext.Set("ActionId", context?.ActionDescriptor.Id);
diagnosticContext.Set("ValidationState", context?.ModelState.IsValid);
}

/// <summary>
Expand Down Expand Up @@ -101,83 +101,141 @@ private static bool MatchesEndpointPattern(HttpContext ctx, string name)
if (endpoint is not RouteEndpoint re) return false;

return string.Equals(
re.RoutePattern?.RawText,
re.RoutePattern.RawText,
name,
StringComparison.Ordinal);
}

private static readonly string[] DefaultFilterTerms = {"password", "token"};

public static Dictionary<string, object> GetLogPropertyDictionary(this object obj) => GetLogPropertyDictionary(obj, DefaultFilterTerms);

/// <summary>
/// Retrieves a dictionary of the values of all public properties we may want to log from an object
/// </summary>
/// <param name="obj"></param>
/// <param name="filtered">By default, this is true, and removes any properties including "password" or "token" in their names</param>
/// <param name="filterTerms">By default, this filters any props containing the terms "password" or "token" if no override is provided</param>
/// <returns></returns>
public static Dictionary<string, object> GetLogPropertyDictionary(this object obj, bool filtered = true)
public static Dictionary<string, object> GetLogPropertyDictionary(this object obj, params string[] filterTerms)
{
if (obj == null) return new Dictionary<string, object>();

var props = TypeDescriptor.GetProperties(obj);
var dict = new Dictionary<string, object>();

foreach (PropertyDescriptor x in props)
foreach (var x in props.Cast<PropertyDescriptor>().Where(x => IsSupportedLogProperty(x.PropertyType)))
{
if (!x.PropertyType.IsPrimitive && x.PropertyType != typeof(string) && x.PropertyType != typeof(DateTime) &&
x.PropertyType != typeof(DateTime?) && x.PropertyType != typeof(int?) &&
x.PropertyType != typeof(bool?)) continue;

if (!filtered || !(x.Name.ToLowerInvariant().Contains("password") ||
x.Name.ToLowerInvariant().Contains("token")))
{
dict.TryAdd(x.Name, x.GetValue(obj));
}
else
{
dict.TryAdd(x.Name, "[Redacted]");
}
dict.TryAdd(x.Name,
filterTerms.Any(y => x.Name.Contains(y, StringComparison.InvariantCultureIgnoreCase))
? "[Redacted]"
: x.GetValue(obj));
}

return dict;
}

internal static bool IsNullable<T>(this T obj, out Type underlying)
{
underlying = null;

if (obj == null)
return true;

var type = typeof(T);
if (!type.IsValueType)
return true;

underlying = Nullable.GetUnderlyingType(type);
return underlying != null;
}

internal static bool IsSupportedLogProperty<T>(this T _) => typeof(T).IsSupportedLogProperty();

internal static bool IsSupportedLogProperty(this Type type)
{
if (type == typeof(string) || type.IsValueType)
return true;

return type.IsNullable(out var underlying) && underlying?.IsSupportedLogProperty() == true;
}

/// <summary>
/// Retrieves a dictionary of all properties between the two objects and, where values differ
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="amended"></param>
/// <param name="original"></param>
/// <param name="filtered">By default, this is true, and removes any properties including "password" or "token" in their names</param>
/// <returns></returns>
public static Dictionary<string, ObjectChange> GetDifference<T>(this T amended, T original, bool filtered = true)
public static Dictionary<string, ObjectChange> GetDifference<T>(this T amended, T original)
{
var originalProps = GetLogPropertyDictionary(original, filtered);
var originalProps = GetLogPropertyDictionary(original);

return GetDifference(amended, originalProps, filtered);
return GetDifference(amended, originalProps);
}

/// <summary>
/// Retrieves a dictionary of all properties between the two objects and, where values differ
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="amended"></param>
/// <param name="original"></param>
/// <param name="filterTerms">By default, this filters any props containing the terms "password" or "token" if no override is provided</param>
/// <returns></returns>
public static Dictionary<string, ObjectChange> GetDifference<T>(this T amended, T original, params string[] filterTerms)
{
var originalProps = GetLogPropertyDictionary(original, filterTerms);

return GetDifference(amended, originalProps, filterTerms);
}

/// <summary>
/// Retrieves a dictionary of all properties in the object (with no original state passed, this will present as all new values)
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="amended"></param>
/// <param name="filtered">By default, this is true, and removes any properties including "password" or "token" in their names</param>
/// <returns></returns>
public static Dictionary<string, ObjectChange> GetDifference<T>(this T amended, bool filtered = true)
public static Dictionary<string, ObjectChange> GetDifference<T>(this T amended)
{
return GetDifference(amended, null, filtered);
return GetDifference(amended, null, DefaultFilterTerms);
}

/// <summary>
/// Retrieves a dictionary of all properties in the object (with no original state passed, this will present as all new values)
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="amended"></param>
/// <param name="filterTerms">By default, this filters any props containing the terms "password" or "token" if no override is provided</param>
/// <returns></returns>
public static Dictionary<string, ObjectChange> GetDifference<T>(this T amended, params string[] filterTerms)
{
return GetDifference(amended, null, filterTerms);
}

/// <summary>
/// Retrieves a dictionary of all properties between the object &amp; original property dictionary and, where values differ
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="amended"></param>
/// <param name="originalProps"></param>
/// <param name="filtered">By default, this is true, and removes any properties including "password" or "token" in their names</param>
/// <returns></returns>
public static Dictionary<string, ObjectChange> GetDifference<T>(this T amended, Dictionary<string, object> originalProps, bool filtered = true)
public static Dictionary<string, ObjectChange> GetDifference<T>(this T amended, Dictionary<string, object> originalProps)
{
var amendedProps = GetLogPropertyDictionary(amended, filtered);
var amendedProps = GetLogPropertyDictionary(amended);

return amendedProps.GetDifference(originalProps);
}

/// <summary>
/// Retrieves a dictionary of all properties between the object &amp; original property dictionary and, where values differ
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="amended"></param>
/// <param name="originalProps"></param>
/// <param name="filterTerms">By default, this filters any props containing the terms "password" or "token" if no override is provided</param>
/// <returns></returns>
public static Dictionary<string, ObjectChange> GetDifference<T>(this T amended, Dictionary<string, object> originalProps, params string[] filterTerms)
{
var amendedProps = GetLogPropertyDictionary(amended, filterTerms);

return amendedProps.GetDifference(originalProps);
}
Expand Down Expand Up @@ -230,7 +288,7 @@ public static Dictionary<string, ObjectChange> GetDifference(this Dictionary<str
/// <returns></returns>
public static Dictionary<string, ObjectChange> GetDifference(this Dictionary<string, object> amendedProps)
{
return amendedProps.GetDifference(null);
return amendedProps.GetDifference((Dictionary<string, object>)null);
}

/// <summary>
Expand Down Expand Up @@ -347,7 +405,7 @@ public static void LogDifference<T>(
/// <summary>
/// Helper to get the basic calling method name
/// </summary>
/// <param name="obj"></param>
/// <param name="_"></param>
/// <param name="memberName"></param>
/// <returns></returns>
public static string MethodName(this object _, [CallerMemberName] string memberName = "") => memberName;
Expand Down
6 changes: 6 additions & 0 deletions src/Pact.Logging/Pact.Logging.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
<PackageTags>Logging</PackageTags>
</PropertyGroup>

<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>Pact.Logging.Tests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
Expand Down
92 changes: 90 additions & 2 deletions test/Pact.Logging.Tests/LoggingExtensionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public void GetPropertyDictionary_NotFiltered_OK()
var obj = new MyClass { Id = 1, Name = "Test", SecurePassword = "blah" };

// act
var dict = obj.GetLogPropertyDictionary(false);
var dict = obj.GetLogPropertyDictionary(Array.Empty<string>());

// assert
dict.Keys.ShouldContain("Id");
Expand All @@ -50,17 +50,20 @@ public void GetDiff_OK()
{
// arrange
var obj = new MyClass { Id = 1, Name = "Test" };
var obj2 = new MyClass { Id = 1, Name = "Tested" };
var obj2 = new MyClass { Id = 1, Name = "Tested", When = DateTime.Now };

// act
var dict = obj2.GetDifference(obj);

// assert
dict.Keys.ShouldContain("Id");
dict.Keys.ShouldContain("Name");
dict.Keys.ShouldContain("When");
dict["Id"].OriginalValue.ShouldBe(1);
dict["Name"].NewValue.ShouldBe("Tested");
dict["Name"].OriginalValue.ShouldBe("Test");
dict["When"].NewValue.ShouldBe(obj2.When);
dict["When"].OriginalValue.ShouldBe(null);
}

[Fact]
Expand Down Expand Up @@ -203,10 +206,95 @@ public void Itsa_Me_Mario_Too()
this.FullMethodName().ShouldBe("Pact.Logging.Tests.LoggingExtensionTests.Itsa_Me_Mario_Too");
}

[Fact]
public void IsSupported_DateTime()
{
DateTime.Now.IsSupportedLogProperty().ShouldBeTrue();
}

[Fact]
public void IsSupported_NullableDateTime()
{
((DateTime?)DateTime.Now).IsSupportedLogProperty().ShouldBeTrue();
}

[Fact]
public void IsSupported_Long()
{
1L.IsSupportedLogProperty().ShouldBeTrue();
}

[Fact]
public void IsSupported_NullableLong()
{
((long?)1).IsSupportedLogProperty().ShouldBeTrue();
}

[Fact]
public void IsSupported_Int()
{
1.IsSupportedLogProperty().ShouldBeTrue();
}

[Fact]
public void IsSupported_NullableInt()
{
((int?)1).IsSupportedLogProperty().ShouldBeTrue();
}

[Fact]
public void IsSupported_Double()
{
1.0D.IsSupportedLogProperty().ShouldBeTrue();
}

[Fact]
public void IsSupported_NullableDouble()
{
((double?)1.0D).IsSupportedLogProperty().ShouldBeTrue();
}

[Fact]
public void IsSupported_Float()
{
1F.IsSupportedLogProperty().ShouldBeTrue();
}

[Fact]
public void IsSupported_NullableFloat()
{
((float?)1.0F).IsSupportedLogProperty().ShouldBeTrue();
}

[Fact]
public void IsSupported_Boolean()
{
true.IsSupportedLogProperty().ShouldBeTrue();
}

[Fact]
public void IsSupported_NullableBoolean()
{
((bool?)true).IsSupportedLogProperty().ShouldBeTrue();
}

[Fact]
public void IsSupported_String()
{
"string".IsSupportedLogProperty().ShouldBeTrue();
}

[Fact]
public void IsNotSupported_Class()
{
new MyClass().IsSupportedLogProperty().ShouldBeFalse();
}

private class MyClass
{
public int Id { get; set; }
public string Name { get; set; }
public string SecurePassword { get; set; }
public DateTime? When { get; set; }
}
}

0 comments on commit b3af038

Please sign in to comment.