Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

${event-properties} - Added ObjectPath for extracting nested object property #3329

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
60 changes: 57 additions & 3 deletions src/NLog/Internal/ObjectReflectionCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -206,9 +206,10 @@ private static FastPropertyLookup[] BuildFastLookup(PropertyInfo[] properties, b

public struct ObjectPropertyList : IEnumerable<ObjectPropertyList.PropertyValue>
{
internal static readonly StringComparer NameComparer = StringComparer.Ordinal;
private readonly object _object;
private readonly PropertyInfo[] _properties;
private readonly ObjectReflectionCache.FastPropertyLookup[] _fastLookup;
private readonly FastPropertyLookup[] _fastLookup;

public struct PropertyValue
{
Expand All @@ -235,6 +236,20 @@ public PropertyValue(string name, object value, TypeCode typeCode)
Value = value;
_typecode = typeCode;
}

public PropertyValue(object owner, PropertyInfo propertyInfo)
{
Name = propertyInfo.Name;
Value = propertyInfo.GetValue(owner, null);
_typecode = TypeCode.Object;
}

public PropertyValue(object owner, FastPropertyLookup fastProperty)
{
Name = fastProperty.Name;
Value = fastProperty.ValueLookup(owner, null);
_typecode = fastProperty.TypeCode;
}
}

public int Count => _fastLookup?.Length ?? _properties?.Length ?? (_object as ICollection)?.Count ?? (_object as ICollection<KeyValuePair<string, object>>)?.Count ?? 0;
Expand All @@ -253,6 +268,43 @@ public ObjectPropertyList(IDictionary<string, object> value)
_fastLookup = null;
}

public bool TryGetPropertyValue(string name, out PropertyValue propertyValue)
{
if (_fastLookup != null)
{
int nameHashCode = NameComparer.GetHashCode(name);
foreach (var fastProperty in _fastLookup)
{
if (fastProperty.NameHashCode==nameHashCode && NameComparer.Equals(fastProperty.Name, name))
{
propertyValue = new PropertyValue(_object, fastProperty);
return true;
}
}
}
else if (_properties != null)
{
foreach (var propInfo in _properties)
{
if (NameComparer.Equals(propInfo.Name, name))
{
propertyValue = new PropertyValue(_object, propInfo);
return true;
}
}
}
else if (_object is IDictionary<string, object> expandoObject)
{
if (expandoObject.TryGetValue(name, out var objectValue))
{
propertyValue = new PropertyValue(name, objectValue, TypeCode.Object);
return true;
}
}
propertyValue = default(PropertyValue);
return false;
}

public override string ToString()
{
return _object?.ToString() ?? "null";
Expand Down Expand Up @@ -302,9 +354,9 @@ public PropertyValue Current
try
{
if (_fastLookup != null)
return new PropertyValue(_fastLookup[_index].Name, _fastLookup[_index].ValueLookup(_owner, null), _fastLookup[_index].TypeCode);
return new PropertyValue(_owner, _fastLookup[_index]);
else if (_properties != null)
return new PropertyValue(_properties[_index].Name, _properties[_index].GetValue(_owner, null), TypeCode.Object);
return new PropertyValue(_owner, _properties[_index]);
else
return new PropertyValue(_enumerator.Current.Key, _enumerator.Current.Value, TypeCode.Object);
}
Expand Down Expand Up @@ -346,12 +398,14 @@ internal struct FastPropertyLookup
public readonly string Name;
public readonly ReflectionHelpers.LateBoundMethod ValueLookup;
public readonly TypeCode TypeCode;
public readonly int NameHashCode;

public FastPropertyLookup(string name, TypeCode typeCode, ReflectionHelpers.LateBoundMethod valueLookup)
{
Name = name;
ValueLookup = valueLookup;
TypeCode = typeCode;
NameHashCode = ObjectPropertyList.NameComparer.GetHashCode(name);
}
}

Expand Down
56 changes: 51 additions & 5 deletions src/NLog/LayoutRenderers/EventPropertiesLayoutRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,24 @@ public class EventPropertiesLayoutRenderer : LayoutRenderer, IRawValue, IStringV
/// <docgen category='Rendering Options' order='100' />
public CultureInfo Culture { get; set; } = CultureInfo.InvariantCulture;

/// <summary>
/// Gets or sets the object-property-navigation-path for lookup of nested property
/// </summary>
/// <docgen category='Rendering Options' order='20' />
public string ObjectPath
{
get => _objectPropertyPath?.Length > 0 ? string.Join(".", _objectPropertyPath) : null;
set => _objectPropertyPath = StringHelpers.IsNullOrWhiteSpace(value) ? null : value.SplitAndTrimTokens('.');
}
private string[] _objectPropertyPath;

private ObjectReflectionCache ObjectReflectionCache => _objectReflectionCache ?? (_objectReflectionCache = new ObjectReflectionCache());
private ObjectReflectionCache _objectReflectionCache;

/// <inheritdoc/>
protected override void Append(StringBuilder builder, LogEventInfo logEvent)
{
if (GetValue(logEvent, out var value))
if (TryGetValue(logEvent, out var value))
{
var formatProvider = GetFormatProvider(logEvent, Culture);
builder.AppendFormattedValue(value, Format, formatProvider);
Expand All @@ -81,24 +95,56 @@ protected override void Append(StringBuilder builder, LogEventInfo logEvent)
/// <inheritdoc/>
bool IRawValue.TryGetRawValue(LogEventInfo logEvent, out object value)
{
GetValue(logEvent, out value);
TryGetValue(logEvent, out value);
return true;
}

/// <inheritdoc/>
string IStringValueRenderer.GetFormattedString(LogEventInfo logEvent) => GetStringValue(logEvent);

private bool GetValue(LogEventInfo logEvent, out object value)
private bool TryGetValue(LogEventInfo logEvent, out object value)
{
value = null;
return logEvent.HasProperties && logEvent.Properties.TryGetValue(Item, out value);

if (!logEvent.HasProperties)
return false;

if (!logEvent.Properties.TryGetValue(Item, out value))
return false;

if (_objectPropertyPath != null && !TryGetObjectProperty(ref value))
return false;

return true;
}

private bool TryGetObjectProperty(ref object value)
{
var objectReflectionCache = ObjectReflectionCache;
for (int i = 0; i < _objectPropertyPath.Length; ++i)
{
if (value == null)
return false;

var eventProperties = objectReflectionCache.LookupObjectProperties(value);
if (eventProperties.TryGetPropertyValue(_objectPropertyPath[i], out var propertyValue))
{
value = propertyValue.Value;
}
else
{
return false;
}
}

return true;
}

private string GetStringValue(LogEventInfo logEvent)
{
if (Format != MessageTemplates.ValueFormatter.FormatAsJson)
{
if (GetValue(logEvent, out var value))
if (TryGetValue(logEvent, out var value))
{
string stringValue = FormatHelper.TryFormatToString(value, Format, GetFormatProvider(logEvent, Culture));
return stringValue;
Expand Down
25 changes: 25 additions & 0 deletions tests/NLog.UnitTests/LayoutRenderers/EventPropertiesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,5 +127,30 @@ public void JsonFormat()
logEvent.Properties["prop1"] = new string[] { "Hello", "World" };
Assert.Equal("[\"Hello\",\"World\"]", layout.Render(logEvent));
}

[Theory]
[InlineData("prop1", "", "{ Id = 1, id = 2, Name = test, Nested = { Id = 3 } }")]
[InlineData("prop1", "Id", "1")]
[InlineData("prop1", "id", "2")] //correct casing
[InlineData("prop1", "Nested.Id", "3")]
[InlineData("prop1", "Id.Wrong", "")] //don't crash on nesting
[InlineData("prop1", "Name", "test")]
[InlineData("prop1", "Name ", "test")] // trimend
[InlineData("prop1", "NameWrong", "")]
public void ObjectPathNestedProperty(string item, string objectPath, string expectedValue)
{
Layout layout = "${event-properties:" + item + ":objectpath=" + objectPath + "}";
layout.Initialize(null);

// Slow Uncached Lookup
LogEventInfo logEvent = LogEventInfo.Create(LogLevel.Info, "logger1", "message1");
logEvent.Properties["prop1"] = new { Id = 1, id = 2, Name = "test", Nested = new { Id = 3 } };
Assert.Equal(expectedValue, layout.Render(logEvent));

// Fast Cached Lookup
LogEventInfo logEvent2 = LogEventInfo.Create(LogLevel.Info, "logger1", "message1");
logEvent2.Properties["prop1"] = new { Id = 1, id = 2, Name = "test", Nested = new { Id = 3 } };
Assert.Equal(expectedValue, layout.Render(logEvent2));
}
}
}