Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,48 @@ var command = new LoginCommand { Username = "logged", Password = "not logged" };
log.Information("Logging in {@Command}", command);
```

#### 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
{
public string Username { get; set; }

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

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

It has effect only on reference types and nullables.

Ignore null properties can be globally applied without applying attributes:
```
Destructurama.Attributed.AttributedDestructuringPolicy.IgnoreNullProperties = true;
```


#### Treating types and properties as scalars

To prevent destructuring of a type or property at all, apply the `[LoggedAsScalar]` attribute.
Expand Down Expand Up @@ -182,3 +224,5 @@ public class CreditCard
public string RegexReplaceSecond { get; set; }
}
```


Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// limitations under the License.

using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
Expand All @@ -24,8 +25,15 @@

namespace Destructurama.Attributed
{
class AttributedDestructuringPolicy : IDestructuringPolicy
public class AttributedDestructuringPolicy : IDestructuringPolicy
{
/// <summary>
/// By setting IgnoreNullProperties to true no need to set [NotLoggedIfDefault] 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 static bool IgnoreNullProperties = false;

readonly static ConcurrentDictionary<Type, CacheEntry> _cache = new ConcurrentDictionary<Type, CacheEntry>();

public bool TryDestructure(object value, ILogEventPropertyValueFactory propertyValueFactory, out LogEventPropertyValue result)
Expand All @@ -37,29 +45,68 @@ public bool TryDestructure(object value, ILogEventPropertyValueFactory propertyV

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

var properties = type.GetPropertiesRecursive().ToList();
if (properties.All(pi => pi.GetCustomAttribute<IPropertyDestructuringAttribute>() == null))
if (!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 CacheEntry((o, f) => MakeStructure(o, properties, destructuringAttributes, f, type));
if (IgnoreNullProperties && !optionalIgnoreAttributes.Any() && !destructuringAttributes.Any())
{
#if NETSTANDARD1_1
if (ti.ImplementedInterfaces.Any(x => x == typeof(IEnumerable)))
#else
if (typeof(IEnumerable).IsAssignableFrom(type))
#endif
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)
static 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 (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 Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using System;
using Serilog.Core;
using Serilog.Events;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// 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
{
public interface IPropertyOptionalIgnoreAttribute
{
bool ShouldPropertyBeIgnored(string name, object value, Type type);
}
}
10 changes: 9 additions & 1 deletion src/Destructurama.Attributed/Attributed/LogMaskedAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// limitations under the License.

using System;
using System.Globalization;
using Serilog.Core;
using Serilog.Events;

Expand Down Expand Up @@ -40,7 +41,14 @@ internal bool IsDefaultMask()

internal object FormatMaskedValue(object propValue)
{
var val = propValue as string;
if (propValue == null)
return null;

string val;
if (propValue is string)
val = propValue as string;
else
val = string.Format(CultureInfo.InvariantCulture, "{0}", propValue);

if (string.IsNullOrEmpty(val))
return val;
Expand Down
11 changes: 3 additions & 8 deletions src/Destructurama.Attributed/Attributed/NotLoggedAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,16 @@
// limitations under the License.

using System;
using Serilog.Core;
using Serilog.Events;

namespace Destructurama.Attributed
{
/// <summary>
/// Specified that a property should not be included when destructuring an object for logging.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class NotLoggedAttribute : Attribute, IPropertyDestructuringAttribute
public class NotLoggedAttribute : Attribute, IPropertyOptionalIgnoreAttribute
{
public bool TryCreateLogEventProperty(string name, object value, ILogEventPropertyValueFactory propertyValueFactory, out LogEventProperty property)
{
property = null;
return false;
}
public bool ShouldPropertyBeIgnored(string name, object value, Type type)
=> true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// 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;
#if NETSTANDARD1_1
using System.Reflection;
#endif

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

class CachedValue<T> : CachedValue
{
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 ConcurrentDictionary<Type, CachedValue>();

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

#if NETSTANDARD1_1
if (type.GetTypeInfo().IsValueType)
#else
if (type.IsValueType)
#endif
{
CachedValue cachedValue;
if (!_cache.TryGetValue(type, out 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 default value for its type should not be included when destructuring an object for logging.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class NotLoggedIfNullAttribute : Attribute, IPropertyOptionalIgnoreAttribute
{
public static readonly NotLoggedIfNullAttribute Instance = new NotLoggedIfNullAttribute();

public bool ShouldPropertyBeIgnored(string name, object value, Type type)
=> value == null;
}
}
Loading