Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 60 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,59 @@ log.Information("Logging in {@Command}", command);
<sup><a href='/test/Destructurama.Attributed.Tests/Snippets.cs#L44-L47' title='Snippet source file'>snippet source</a> | <a href='#snippet-logcommand' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

#### Ignoring a property if it's the default value

Apply the `NotLoggedIfDefault` attribute:

```csharp
public class LoginCommand
{
public string Username { get; set; }

[NotLoggedIfDefault]
public string Password { get; set; }

[NotLoggedIfDefault]
public DateTime TimeStamp { get; set; }
}
```

#### Ignoring a property if it has the null value

Apply the `NotLoggedIfNull` attribute:

```csharp
public class LoginCommand
{
/// <summary>
/// `null` value results in removed property
/// </summary>
[NotLoggedIfNull]
public string Username { get; set; }

/// <summary>
/// Can be applied with [LogMasked] or [LogReplaced] attributes
/// `null` value results in removed property
/// "123456789" results in "***"
/// </summary>
[NotLoggedIfNull] [LogMasked]
public string Password { get; set; }

/// <summary>
/// Attribute has no effect on non-reference and non-nullable types
/// </summary>
[NotLoggedIfNull]
public int TimeStamp { get; set; }
}
```

Ignore null properties can be globally applied during initialization without need to apply attributes:
```csharp
var log = new LoggerConfiguration()
.Destructure.UsingAttributes(x => x.IgnoreNullProperties = true)
...
```


## Treating types and properties as scalars

Expand Down Expand Up @@ -112,6 +165,12 @@ public class CustomizedMaskedLogs
[LogMasked(PreserveLength = true)]
public string? DefaultMaskedPreserved { get; set; }

/// <summary>
/// "" results in "***"
/// </summary>
[LogMasked]
public string? DefaultMaskedNotPreservedOnEmptyString { get; set; }

/// <summary>
/// 123456789 results in "#"
/// </summary>
Expand Down Expand Up @@ -203,7 +262,7 @@ public class CustomizedMaskedLogs
public string? ShowFirstAndLastThreeAndCustomMaskInTheMiddlePreservedLengthIgnored { get; set; }
}
```
<sup><a href='/test/Destructurama.Attributed.Tests/MaskedAttributeTests.cs#L9-L122' title='Snippet source file'>snippet source</a> | <a href='#snippet-customizedmaskedlogs' title='Start of snippet'>anchor</a></sup>
<sup><a href='/test/Destructurama.Attributed.Tests/MaskedAttributeTests.cs#L10-L129' title='Snippet source file'>snippet source</a> | <a href='#snippet-customizedmaskedlogs' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->


Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2015-2018 Destructurama Contributors, Serilog Contributors
// Copyright 2015-2018 Destructurama Contributors, Serilog Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -13,6 +13,7 @@
// limitations under the License.

using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
Expand All @@ -28,6 +29,18 @@ namespace Destructurama.Attributed
class AttributedDestructuringPolicy : IDestructuringPolicy
{
readonly static ConcurrentDictionary<Type, CacheEntry> _cache = new();
private readonly AttributedDestructuringPolicyOptions _options;

public AttributedDestructuringPolicy()
{
_options = new AttributedDestructuringPolicyOptions();
}

public AttributedDestructuringPolicy(Action<AttributedDestructuringPolicyOptions> configure)
: this()
{
configure?.Invoke(_options);
}

public bool TryDestructure(object value, ILogEventPropertyValueFactory propertyValueFactory, [NotNullWhen(true)] out LogEventPropertyValue? result)
{
Expand All @@ -36,31 +49,66 @@ public bool TryDestructure(object value, ILogEventPropertyValueFactory propertyV
return cached.CanDestructure;
}

static CacheEntry CreateCacheEntry(Type type)
private CacheEntry CreateCacheEntry(Type type)
{
var classDestructurer = type.GetTypeInfo().GetCustomAttribute<ITypeDestructuringAttribute>();
var ti = type.GetTypeInfo();
var classDestructurer = ti.GetCustomAttribute<ITypeDestructuringAttribute>();
if (classDestructurer != null)
return new((o, f) => classDestructurer.CreateLogEventPropertyValue(o, f));

var properties = type.GetPropertiesRecursive().ToList();
if (properties.All(pi => pi.GetCustomAttribute<IPropertyDestructuringAttribute>() == null))
if (!_options.IgnoreNullProperties
&& properties.All(pi =>
pi.GetCustomAttribute<IPropertyDestructuringAttribute>() == null
&& pi.GetCustomAttribute<IPropertyOptionalIgnoreAttribute>() == null))
{
return CacheEntry.Ignore;
}

var optionalIgnoreAttributes = properties
.Select(pi => new { pi, Attribute = pi.GetCustomAttribute<IPropertyOptionalIgnoreAttribute>() })
.Where(o => o.Attribute != null)
.ToDictionary(o => o.pi, o => o.Attribute);

var destructuringAttributes = properties
.Select(pi => new { pi, Attribute = pi.GetCustomAttribute<IPropertyDestructuringAttribute>() })
.Where(o => o.Attribute != null)
.ToDictionary(o => o.pi, o => o.Attribute);

return new((o, f) => MakeStructure(o, properties, destructuringAttributes, f, type));
if (_options.IgnoreNullProperties && !optionalIgnoreAttributes.Any() && !destructuringAttributes.Any())
{
if (typeof(IEnumerable).IsAssignableFrom(type))
return CacheEntry.Ignore;
}

return new CacheEntry((o, f) => MakeStructure(o, properties, optionalIgnoreAttributes, destructuringAttributes, f, type));
}

static LogEventPropertyValue MakeStructure(object o, IEnumerable<PropertyInfo> loggedProperties, IDictionary<PropertyInfo, IPropertyDestructuringAttribute> destructuringAttributes, ILogEventPropertyValueFactory propertyValueFactory, Type type)
private LogEventPropertyValue MakeStructure(
object o,
IEnumerable<PropertyInfo> loggedProperties,
IDictionary<PropertyInfo, IPropertyOptionalIgnoreAttribute> optionalIgnoreAttributes,
IDictionary<PropertyInfo, IPropertyDestructuringAttribute> destructuringAttributes,
ILogEventPropertyValueFactory propertyValueFactory,
Type type)
{
var structureProperties = new List<LogEventProperty>();
foreach (var pi in loggedProperties)
{
var propValue = SafeGetPropValue(o, pi);

if (optionalIgnoreAttributes.TryGetValue(pi, out var optionalIgnoreAttribute))
{
if (optionalIgnoreAttribute.ShouldPropertyBeIgnored(pi.Name, propValue, pi.PropertyType))
continue;
}

if (_options.IgnoreNullProperties)
{
if (NotLoggedIfNullAttribute.Instance.ShouldPropertyBeIgnored(pi.Name, propValue, pi.PropertyType))
continue;
}

if (destructuringAttributes.TryGetValue(pi, out var destructuringAttribute))
{
if (destructuringAttribute.TryCreateLogEventProperty(pi.Name, propValue, propertyValueFactory, out var property))
Expand All @@ -87,5 +135,10 @@ static object SafeGetPropValue(object o, PropertyInfo pi)
return $"The property accessor threw an exception: {ex.InnerException!.GetType().Name}";
}
}

internal static void Clear()
{
_cache.Clear();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Destructurama.Attributed
{
/// <summary>
/// Global destructuring options.
/// </summary>
public class AttributedDestructuringPolicyOptions
{
/// <summary>
/// By setting IgnoreNullProperties to true no need to set [NotLoggedIfNull] for every logged property.
/// Custom types implemenenting IEnumerable, will be destructed as StructureValue and affected by IgnoreNullProperties
/// only in case at least one property (or the type itself) has Destructurama attribute applied.
/// </summary>
public bool IgnoreNullProperties { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright 2020 Destructurama Contributors, Serilog Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System;

namespace Destructurama.Attributed
{
/// <summary>
/// Base interfaces for all <see cref="Attribute"/>s that determine should a property be ignored.
/// </summary>
public interface IPropertyOptionalIgnoreAttribute
{
/// <summary>
/// Determine should a property be ignored
/// </summary>
/// <param name="name">The current property name</param>
/// <param name="value">The current property value</param>
/// <param name="type">The current property type</param>
/// <returns></returns>
bool ShouldPropertyBeIgnored(string name, object? value, Type type);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright 2020 Destructurama Contributors, Serilog Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System;
using System.Collections.Concurrent;

namespace Destructurama.Attributed
{
abstract class CachedValue
{
public abstract bool IsDefaultValue(object value);
}

class CachedValue<T> : CachedValue where T: notnull
{
T Value { get; set; }

public CachedValue(T value)
{
Value = value;
}

public override bool IsDefaultValue(object value)
{
return Value.Equals(value);
}
}

/// <summary>
/// Specified that a property with default value for its type should not be included when destructuring an object for logging.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class NotLoggedIfDefaultAttribute : Attribute, IPropertyOptionalIgnoreAttribute
{
readonly static ConcurrentDictionary<Type, CachedValue> _cache = new();

bool IPropertyOptionalIgnoreAttribute.ShouldPropertyBeIgnored(string name, object? value, Type type)
{
if (value != null)
{

if (type.IsValueType)
{
if (!_cache.TryGetValue(type, out CachedValue cachedValue))
{
var cachedValueType = typeof(CachedValue<>).MakeGenericType(type);
var defaultValue = Activator.CreateInstance(type);
cachedValue = (CachedValue)Activator.CreateInstance(cachedValueType, defaultValue);

_cache.TryAdd(type, cachedValue);
}

if (cachedValue.IsDefaultValue(value))
{
return true;
}
}

return false;
}

return true;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright 2020 Destructurama Contributors, Serilog Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System;

namespace Destructurama.Attributed
{
/// <summary>
/// Specified that a property with null value should not be included when destructuring an object for logging.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class NotLoggedIfNullAttribute : Attribute, IPropertyOptionalIgnoreAttribute
{
internal static readonly IPropertyOptionalIgnoreAttribute Instance = new NotLoggedIfNullAttribute();

bool IPropertyOptionalIgnoreAttribute.ShouldPropertyBeIgnored(string name, object? value, Type type)
=> value == null;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2014-2018 Destructurama Contributors, Serilog Contributors
// Copyright 2014-2018 Destructurama Contributors, Serilog Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -15,7 +15,11 @@
using Destructurama.Attributed;
using Serilog;
using Serilog.Configuration;
using System;
using Serilog.Core;
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Destructurama.Attributed.Tests, PublicKey = 0024000004800000940000000602000000240000525341310004000001000100638a43140e8a1271c1453df1379e64b40b67a1f333864c1aef5ac318a0fa2008545c3d35a82ef005edf0de1ad1e1ea155722fe289df0e462f78c40a668cbc96d7be1d487faef5714a54bb4e57909c86b3924c2db6d55ccf59939b99eb0cab6e8a91429ba0ce630c08a319b323bddcbbd509f1afe4ae77a6cbb8b447f588febc3")]

namespace Destructurama
{
Expand All @@ -32,5 +36,18 @@ public static class LoggerConfigurationAppSettingsExtensions
/// <returns>An object allowing configuration to continue.</returns>
public static LoggerConfiguration UsingAttributes(this LoggerDestructuringConfiguration configuration) =>
configuration.With<AttributedDestructuringPolicy>();


/// <summary>
/// </summary>
/// <param name="configuration">The logger configuration to apply configuration to.</param>
/// <param name="configure">Configure Destructurama options</param>
/// <returns>An object allowing configuration to continue.</returns>
public static LoggerConfiguration UsingAttributes(this LoggerDestructuringConfiguration configuration,
Action<AttributedDestructuringPolicyOptions> configure)
{
var policy = new AttributedDestructuringPolicy(configure);
return configuration.With(policy);
}
}
}
Loading