Skip to content

Commit cb13e4b

Browse files
authored
Add NotLoggedIfNull and NotLoggedIfDefault attributes (#49)
1 parent 597c344 commit cb13e4b

10 files changed

+1029
-10
lines changed

README.md

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,59 @@ log.Information("Logging in {@Command}", command);
7575
<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>
7676
<!-- endSnippet -->
7777

78+
#### Ignoring a property if it has the default value
79+
80+
Apply the `NotLoggedIfDefault` attribute:
81+
82+
```csharp
83+
public class LoginCommand
84+
{
85+
public string Username { get; set; }
86+
87+
[NotLoggedIfDefault]
88+
public string Password { get; set; }
89+
90+
[NotLoggedIfDefault]
91+
public DateTime TimeStamp { get; set; }
92+
}
93+
```
94+
95+
#### Ignoring a property if it has the null value
96+
97+
Apply the `NotLoggedIfNull` attribute:
98+
99+
```csharp
100+
public class LoginCommand
101+
{
102+
/// <summary>
103+
/// `null` value results in removed property
104+
/// </summary>
105+
[NotLoggedIfNull]
106+
public string Username { get; set; }
107+
108+
/// <summary>
109+
/// Can be applied with [LogMasked] or [LogReplaced] attributes
110+
/// `null` value results in removed property
111+
/// "123456789" results in "***"
112+
/// </summary>
113+
[NotLoggedIfNull] [LogMasked]
114+
public string Password { get; set; }
115+
116+
/// <summary>
117+
/// Attribute has no effect on non-reference and non-nullable types
118+
/// </summary>
119+
[NotLoggedIfNull]
120+
public int TimeStamp { get; set; }
121+
}
122+
```
123+
124+
Ignore null properties can be globally applied during initialization without need to apply attributes:
125+
```csharp
126+
var log = new LoggerConfiguration()
127+
.Destructure.UsingAttributes(x => x.IgnoreNullProperties = true)
128+
...
129+
```
130+
78131

79132
## Treating types and properties as scalars
80133

@@ -120,6 +173,12 @@ public class CustomizedMaskedLogs
120173
[LogMasked(PreserveLength = true)]
121174
public string? DefaultMaskedPreserved { get; set; }
122175

176+
/// <summary>
177+
/// "" results in "***"
178+
/// </summary>
179+
[LogMasked]
180+
public string? DefaultMaskedNotPreservedOnEmptyString { get; set; }
181+
123182
/// <summary>
124183
/// 123456789 results in "#"
125184
/// </summary>
@@ -211,7 +270,7 @@ public class CustomizedMaskedLogs
211270
public string? ShowFirstAndLastThreeAndCustomMaskInTheMiddlePreservedLengthIgnored { get; set; }
212271
}
213272
```
214-
<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>
273+
<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>
215274
<!-- endSnippet -->
216275

217276

src/Destructurama.Attributed/Attributed/AttributedDestructuringPolicy.cs

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2015-2018 Destructurama Contributors, Serilog Contributors
1+
// Copyright 2015-2018 Destructurama Contributors, Serilog Contributors
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -13,6 +13,7 @@
1313
// limitations under the License.
1414

1515
using System;
16+
using System.Collections;
1617
using System.Collections.Concurrent;
1718
using System.Collections.Generic;
1819
using System.Diagnostics.CodeAnalysis;
@@ -28,6 +29,18 @@ namespace Destructurama.Attributed
2829
class AttributedDestructuringPolicy : IDestructuringPolicy
2930
{
3031
readonly static ConcurrentDictionary<Type, CacheEntry> _cache = new();
32+
private readonly AttributedDestructuringPolicyOptions _options;
33+
34+
public AttributedDestructuringPolicy()
35+
{
36+
_options = new AttributedDestructuringPolicyOptions();
37+
}
38+
39+
public AttributedDestructuringPolicy(Action<AttributedDestructuringPolicyOptions> configure)
40+
: this()
41+
{
42+
configure?.Invoke(_options);
43+
}
3144

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

39-
static CacheEntry CreateCacheEntry(Type type)
52+
private CacheEntry CreateCacheEntry(Type type)
4053
{
41-
var classDestructurer = type.GetTypeInfo().GetCustomAttribute<ITypeDestructuringAttribute>();
54+
var ti = type.GetTypeInfo();
55+
var classDestructurer = ti.GetCustomAttribute<ITypeDestructuringAttribute>();
4256
if (classDestructurer != null)
4357
return new((o, f) => classDestructurer.CreateLogEventPropertyValue(o, f));
4458

4559
var properties = type.GetPropertiesRecursive().ToList();
46-
if (properties.All(pi => pi.GetCustomAttribute<IPropertyDestructuringAttribute>() == null))
60+
if (!_options.IgnoreNullProperties
61+
&& properties.All(pi =>
62+
pi.GetCustomAttribute<IPropertyDestructuringAttribute>() == null
63+
&& pi.GetCustomAttribute<IPropertyOptionalIgnoreAttribute>() == null))
64+
{
4765
return CacheEntry.Ignore;
66+
}
67+
68+
var optionalIgnoreAttributes = properties
69+
.Select(pi => new { pi, Attribute = pi.GetCustomAttribute<IPropertyOptionalIgnoreAttribute>() })
70+
.Where(o => o.Attribute != null)
71+
.ToDictionary(o => o.pi, o => o.Attribute);
4872

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

54-
return new((o, f) => MakeStructure(o, properties, destructuringAttributes, f, type));
78+
if (_options.IgnoreNullProperties && !optionalIgnoreAttributes.Any() && !destructuringAttributes.Any())
79+
{
80+
if (typeof(IEnumerable).IsAssignableFrom(type))
81+
return CacheEntry.Ignore;
82+
}
83+
84+
return new CacheEntry((o, f) => MakeStructure(o, properties, optionalIgnoreAttributes, destructuringAttributes, f, type));
5585
}
5686

57-
static LogEventPropertyValue MakeStructure(object o, IEnumerable<PropertyInfo> loggedProperties, IDictionary<PropertyInfo, IPropertyDestructuringAttribute> destructuringAttributes, ILogEventPropertyValueFactory propertyValueFactory, Type type)
87+
private LogEventPropertyValue MakeStructure(
88+
object o,
89+
IEnumerable<PropertyInfo> loggedProperties,
90+
IDictionary<PropertyInfo, IPropertyOptionalIgnoreAttribute> optionalIgnoreAttributes,
91+
IDictionary<PropertyInfo, IPropertyDestructuringAttribute> destructuringAttributes,
92+
ILogEventPropertyValueFactory propertyValueFactory,
93+
Type type)
5894
{
5995
var structureProperties = new List<LogEventProperty>();
6096
foreach (var pi in loggedProperties)
6197
{
6298
var propValue = SafeGetPropValue(o, pi);
6399

100+
if (optionalIgnoreAttributes.TryGetValue(pi, out var optionalIgnoreAttribute))
101+
{
102+
if (optionalIgnoreAttribute.ShouldPropertyBeIgnored(pi.Name, propValue, pi.PropertyType))
103+
continue;
104+
}
105+
106+
if (_options.IgnoreNullProperties)
107+
{
108+
if (NotLoggedIfNullAttribute.Instance.ShouldPropertyBeIgnored(pi.Name, propValue, pi.PropertyType))
109+
continue;
110+
}
111+
64112
if (destructuringAttributes.TryGetValue(pi, out var destructuringAttribute))
65113
{
66114
if (destructuringAttribute.TryCreateLogEventProperty(pi.Name, propValue, propertyValueFactory, out var property))
@@ -87,5 +135,10 @@ static object SafeGetPropValue(object o, PropertyInfo pi)
87135
return $"The property accessor threw an exception: {ex.InnerException!.GetType().Name}";
88136
}
89137
}
138+
139+
internal static void Clear()
140+
{
141+
_cache.Clear();
142+
}
90143
}
91144
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace Destructurama.Attributed
2+
{
3+
/// <summary>
4+
/// Global destructuring options.
5+
/// </summary>
6+
public class AttributedDestructuringPolicyOptions
7+
{
8+
/// <summary>
9+
/// By setting IgnoreNullProperties to true no need to set [NotLoggedIfNull] for every logged property.
10+
/// Custom types implementing IEnumerable, will be destructed as StructureValue and affected by IgnoreNullProperties
11+
/// only in case at least one property (or the type itself) has Destructurama attribute applied.
12+
/// </summary>
13+
public bool IgnoreNullProperties { get; set; }
14+
}
15+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright 2020 Destructurama Contributors, Serilog Contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using System;
16+
17+
namespace Destructurama.Attributed
18+
{
19+
/// <summary>
20+
/// Base interfaces for all <see cref="Attribute"/>s that determine should a property be ignored.
21+
/// </summary>
22+
public interface IPropertyOptionalIgnoreAttribute
23+
{
24+
/// <summary>
25+
/// Determine should a property be ignored
26+
/// </summary>
27+
/// <param name="name">The current property name</param>
28+
/// <param name="value">The current property value</param>
29+
/// <param name="type">The current property type</param>
30+
/// <returns></returns>
31+
bool ShouldPropertyBeIgnored(string name, object? value, Type type);
32+
}
33+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright 2020 Destructurama Contributors, Serilog Contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using System;
16+
using System.Collections.Concurrent;
17+
18+
namespace Destructurama.Attributed
19+
{
20+
abstract class CachedValue
21+
{
22+
public abstract bool IsDefaultValue(object value);
23+
}
24+
25+
class CachedValue<T> : CachedValue where T: notnull
26+
{
27+
T Value { get; set; }
28+
29+
public CachedValue(T value)
30+
{
31+
Value = value;
32+
}
33+
34+
public override bool IsDefaultValue(object value)
35+
{
36+
return Value.Equals(value);
37+
}
38+
}
39+
40+
/// <summary>
41+
/// Specified that a property with default value for its type should not be included when destructuring an object for logging.
42+
/// </summary>
43+
[AttributeUsage(AttributeTargets.Property)]
44+
public class NotLoggedIfDefaultAttribute : Attribute, IPropertyOptionalIgnoreAttribute
45+
{
46+
readonly static ConcurrentDictionary<Type, CachedValue> _cache = new();
47+
48+
bool IPropertyOptionalIgnoreAttribute.ShouldPropertyBeIgnored(string name, object? value, Type type)
49+
{
50+
if (value != null)
51+
{
52+
53+
if (type.IsValueType)
54+
{
55+
if (!_cache.TryGetValue(type, out CachedValue cachedValue))
56+
{
57+
var cachedValueType = typeof(CachedValue<>).MakeGenericType(type);
58+
var defaultValue = Activator.CreateInstance(type);
59+
cachedValue = (CachedValue)Activator.CreateInstance(cachedValueType, defaultValue);
60+
61+
_cache.TryAdd(type, cachedValue);
62+
}
63+
64+
if (cachedValue.IsDefaultValue(value))
65+
{
66+
return true;
67+
}
68+
}
69+
70+
return false;
71+
}
72+
73+
return true;
74+
}
75+
}
76+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright 2020 Destructurama Contributors, Serilog Contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using System;
16+
17+
namespace Destructurama.Attributed
18+
{
19+
/// <summary>
20+
/// Specified that a property with null value should not be included when destructuring an object for logging.
21+
/// </summary>
22+
[AttributeUsage(AttributeTargets.Property)]
23+
public class NotLoggedIfNullAttribute : Attribute, IPropertyOptionalIgnoreAttribute
24+
{
25+
internal static readonly IPropertyOptionalIgnoreAttribute Instance = new NotLoggedIfNullAttribute();
26+
27+
bool IPropertyOptionalIgnoreAttribute.ShouldPropertyBeIgnored(string name, object? value, Type type)
28+
=> value == null;
29+
}
30+
}

src/Destructurama.Attributed/LoggerConfigurationAttributedExtensions.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2014-2018 Destructurama Contributors, Serilog Contributors
1+
// Copyright 2014-2018 Destructurama Contributors, Serilog Contributors
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -15,7 +15,11 @@
1515
using Destructurama.Attributed;
1616
using Serilog;
1717
using Serilog.Configuration;
18+
using System;
1819
using Serilog.Core;
20+
using System.Runtime.CompilerServices;
21+
22+
[assembly: InternalsVisibleTo("Destructurama.Attributed.Tests, PublicKey = 0024000004800000940000000602000000240000525341310004000001000100638a43140e8a1271c1453df1379e64b40b67a1f333864c1aef5ac318a0fa2008545c3d35a82ef005edf0de1ad1e1ea155722fe289df0e462f78c40a668cbc96d7be1d487faef5714a54bb4e57909c86b3924c2db6d55ccf59939b99eb0cab6e8a91429ba0ce630c08a319b323bddcbbd509f1afe4ae77a6cbb8b447f588febc3")]
1923

2024
namespace Destructurama
2125
{
@@ -32,5 +36,18 @@ public static class LoggerConfigurationAppSettingsExtensions
3236
/// <returns>An object allowing configuration to continue.</returns>
3337
public static LoggerConfiguration UsingAttributes(this LoggerDestructuringConfiguration configuration) =>
3438
configuration.With<AttributedDestructuringPolicy>();
39+
40+
41+
/// <summary>
42+
/// </summary>
43+
/// <param name="configuration">The logger configuration to apply configuration to.</param>
44+
/// <param name="configure">Configure Destructurama options</param>
45+
/// <returns>An object allowing configuration to continue.</returns>
46+
public static LoggerConfiguration UsingAttributes(this LoggerDestructuringConfiguration configuration,
47+
Action<AttributedDestructuringPolicyOptions> configure)
48+
{
49+
var policy = new AttributedDestructuringPolicy(configure);
50+
return configuration.With(policy);
51+
}
3552
}
3653
}

0 commit comments

Comments
 (0)